LLM 에 단순히 질문하는 것 보다는 정보를 같이 제공함으로써 얻는 결과는 보다 구체적이고 사실적인 결과를 얻을 수 있다.
이때 정보는 문장이 벡터화된 DB로부터 유의도가 높은 순서대로 검색되게 된다. 이를 RAG 이라 한다.
정보 데이터(PDF) 등록
⇒ gemini 또는 OpenAI embedding 모델사용
⇒ Embedding 데이터베이스는 Qdrant 클라우드 FREE TIER 이용
정보검색
⇒ 질문을 그대로 검색하기 보단 agno Agent를 통해 질문의 의도를 파악하여 구체적인 질문으로 변환
⇒ 변환된 질문을 Embedding 데이터베이스로 부터 유이도가 높은 검색 결과를 이용하여 agno Agent 를 통해 정리
Qdrant
https://qdrant.tech/ 에서 구글계정으로 Sign Up하면 FREE TIER 를 제공받는다.
LangChain
pip install qdrant-client pip install langchain-qdrant pip install langchain-community |
agno
import
import os
import tempfile
from datetime import datetime
from typing import List
import google.generativeai as genai
from agno.agent import Agent
from agno.models.google import Gemini
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_qdrant import QdrantVectorStore
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams
from langchain_core.embeddings import Embeddings
QdrantClient 생성
qdrant_client = QdrantClient(
url="Qdrant에서 생성한 Cluster의 Endpoint",
api_key="Qdrant에서 생성한 Cluster의 API key",
timeout=60
)
아래 생성할 QdrantVectorStore 의 embedding 파라미터에 지정할 Embeddings의 클래스를 정의
※ langchain_core.embeddings.Embeddings 클래스를 계승하고 override 메서드는 아래와 같다.
embed_documents(texts) : Embed search docs.
embed_query(text) : Embed query text.
※ 본인의 테스트 결과로는 dimension이 두배인 openai 의 embedding 결과가 좋게 나왔다.
class GeminiEmbedder(Embeddings):
def __init__(self, model_name="models/text-embedding-004"):
genai.configure(api_key=st.session_state.google_api_key)
self.model = model_name
def embed_documents(self, texts: List[str]) -> List[List[float]]:
return [self.embed_query(text) for text in texts]
def embed_query(self, text: str) -> List[float]:
response = genai.embed_content(
model=self.model,
content=text,
task_type="retrieval_document"
)
return response['embedding']
class OpenAIEmbedder(Embeddings):
def __init__(self, model_name="text-embedding-3-small"):
self.openai_client = openai.Client(api_key="openai_api_key")
self.model = model_name
def embed_documents(self, texts: List[str]) -> List[List[float]]:
return [self.embed_query(text) for text in texts]
def embed_query(self, text: str) -> List[float]:
response = self.openai_client.embeddings.create(
model=self.model,
input=text,
).data[0].embedding
return response
맨 처음엔 Qdrant 에 Collection을 생성
qdran_client .create_collection(
collection_name= "저장될이름 보통 db의 테이명이라 보면 된다." ,
vectors_config=VectorParams(
size=768, # 중요: gemini는 768, openai는 1536 이다.
distance=Distance.COSINE
)
QdrantVectorStore 생성
⇒ PDF로 분할된 텍스트의 벡터화와 Qdrant에 저장
⇒ 검색
vector_store = QdrantVectorStore(
client=qdran_client,
collection_name="저장될이름 보통 db의 테이명이라 보면 된다.",
embedding=GeminiEmbedder() 또는 OpenAIEmbedder()
)
Agno Agents
1. agent_rewriter
agent_rewriter = Agent(
name="Query Rewriter",
model=Gemini(id="gemini-exp-1206"),
instructions="""당신은 질문을 더 명확하고 구체적으로 재구성하는 전문가입니다.
당신의 임무는 다음과 같습니다:
사용자의 질문을 분석한다.
질문을 더 구체적이고 검색 친화적으로 다시 작성한다.
약어나 기술 용어가 있다면 풀어서 쓴다.
추가적인 설명이나 텍스트 없이, 다시 작성된 질문만 반환한다.
예시 1:
사용자: "ML에 대해 뭐라고 되어 있어?"
출력: "Machine Learning(ML)의 주요 개념, 기술, 그리고 응용 분야에 대해 문맥에서 논의된 내용을 설명하라."
예시 2:
사용자: "transformers에 대해 말해줘"
출력: "자연어 처리와 딥러닝에서 Transformer 신경망의 구조, 작동 원리, 그리고 응용 분야를 설명하라."
""",
show_tool_calls=False,
markdown=True,
)
2. agent_rag
agent_rag = Agent(
name="Gemini RAG Agent",
model=Gemini(id="gemini-2.0-flash-thinking-exp-01-21"),
instructions="""당신은 정확한 답변을 제공하는 지능형 에이전트(Intelligent Agent) 입니다.
다음 지침을 따르십시오:
문서로부터 문맥이 주어졌을 때:
- 제공된 문서의 정보에 집중한다.
- 구체적이고 정확한 세부 사항을 인용하여 답변한다.
웹 검색 결과가 주어졌을 때:
- 해당 정보가 웹 검색을 통해 얻은 것임을 명확히 표시한다.
- 검색 결과를 명료하게 종합하여 제시한다.
항상 높은 정확성(accuracy) 과 명확성(clarity) 을 유지하여 응답해야 한다
""",
show_tool_calls=True,
markdown=True,
)
PDF 화일의 텍스트화와 분할
def process_pdf(file) -> List:
"""Process PDF file and add source metadata."""
try:
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file:
tmp_file.write(file.getvalue())
loader = PyPDFLoader(tmp_file.name)
documents = loader.load()
# Add source metadata
for doc in documents:
doc.metadata.update({
"source_type": "pdf",
"file_name": file.name,
"timestamp": datetime.now().isoformat()
})
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
return text_splitter.split_documents(documents)
벡터화 저장
texts = process_pdf(uploaded_file)
vector_store.add_documents(texts)
질문의 재정의와 벡터화 후 검색 (상위 5개, 유의도는 0.7 이상)
context = ""
rewritten_query = agent_rewriter.run(prompt).content
retriever = vector_store.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={
"k": 5,
"score_threshold": 0.7
}
)
docs = retriever.invoke(rewritten_query)
if docs:
context = "\n\n".join([d.page_content for d in docs])
최종 결과
full_prompt = f"""Context: {context}
Original Question: {prompt}
Rewritten Question: {rewritten_query}
Please provide a comprehensive answer based on the available information.
"""
response = agent_ rag.run(full_prompt)
print(response.content)