LangChain RAG 챗봇 시스템은 Upstage의 Solar API를 핵심 AI 엔진으로 사용합니다. Solar API는 두 가지 주요 기능을 제공합니다:
1. Language Model (LLM): 답변 생성 및 텍스트 처리
2. Embedding Model: 문서와 질문의 벡터화
llm.pysolar-pro 또는 solar-1-mini-chatget_llm()model = "solar-pro"
temperature = 0.1 # 낮은 값으로 정확성 중시
max_tokens = 2000
streaming = Falsetext_to_sql.pysolar-proget_sql_llm()model = "solar-pro"
temperature = 0.0 # 완전 결정적 출력
max_tokens = 1000
streaming = Falsellm.pysolar-embedding-1-largeget_embeddings()solar-embedding-1-large: 일반용solar-embedding-1-large-query: 쿼리 최적화solar-embedding-1-large-passage: 문서 최적화[사용자 질문]
↓
[1. 전처리]
↓
[2. 검색 (Retrieval)]
├─→ Vector Search (ChromaDB)
└─→ SQL Search (SQLite)
↓
[3. 컨텍스트 생성]
↓
[4. 프롬프트 구성]
↓
[5. 생성 (Generation)]
↓
[6. 후처리]
↓
[최종 답변]
demo_with_memory.py (라인 249-255)retriever.py (라인 45-150)# retriever.py - vector_search() 메서드
def vector_search(query: str, k: int = 5):
# 1. 질문을 Solar Embedding으로 벡터화
query_vector = embeddings.embed_query(query)
# 2. ChromaDB에서 유사도 검색
docs_with_scores = vector_db.search_with_scores(query, k=k)
# 3. 점수 기반 필터링 및 정렬
return filtered_results
# retriever.py - sql_search() 메서드
def sql_search(query: str, k: int = 5):
# 키워드 기반 SQLite 검색
sql_query = """
SELECT doc_id, title, url, content,
(CASE
WHEN title LIKE ? THEN 3
WHEN content LIKE ? THEN 1
ELSE 0
END) as relevance_score
FROM documents
WHERE title LIKE ? OR content LIKE ?
ORDER BY relevance_score DESC
LIMIT ?
"""
return sql_results
# retriever.py - search() 메서드
def search(query: str, k: int = 5, mode: str = "hybrid"):
vector_results = vector_search(query, k)
sql_results = sql_search(query, k)
# 중복 제거 및 점수 기반 재정렬
merged_results = merge_and_rank(vector_results, sql_results)
return merged_results[:k]
위치: demo_with_memory.py (라인 279-291)
처리 내용:
# 검색된 문서들을 컨텍스트로 조합
context = "\n\n".join([doc.page_content[:500] for doc in docs])
# 소스 정보 수집
sources = []
for doc in docs[:3]:
title = doc.metadata.get('title', 'Unknown')
url = doc.metadata.get('url', '#')
sources.append(f"[{title}]({url})")
위치: demo_with_memory.py - generate_response_with_memory() (라인 124-145)
프롬프트 템플릿:
prompt = f"""당신은 LangChain 전문가 AI 어시스턴트입니다.
아래 제공된 컨텍스트와 이전 대화 내용을 참고하여 사용자의 질문에 답변해주세요.
{conversation_history}
현재 컨텍스트:
{context}
현재 질문: {query}
지침:
1. 이전 대화 맥락을 고려하여 답변하세요
2. 사용자가 "그것", "이것", "위의" 등 지시대명사를 사용하면 이전 대화에서 언급된 내용을 참고하세요
3. 연속된 질문인 경우 이전 답변을 바탕으로 더 자세히 설명하세요
4. 컨텍스트에 없는 내용은 추론하지 말고 모른다고 답하세요
답변:"""
위치: demo_with_memory.py (라인 146-152)
Solar LLM 호출:
# Solar LLM으로 답변 생성
response = st.session_state.llm.invoke(prompt)
# 대화 관리자에 기록
if st.session_state.conversation_manager:
conversation_manager.add_user_message(query)
conversation_manager.add_assistant_message(response.content)
demo_with_memory.py (라인 314-323)# 1. 사용자 입력
user_query = "LangChain에서 메모리를 사용하는 방법"
# 2. 임베딩 생성 (Solar Embedding API 호출 #1)
query_embedding = solar_embedding.embed_query(user_query)
# → 4096차원 벡터 생성
# 3. Vector Search
vector_results = chromadb.similarity_search(
query_embedding,
k=5,
filter={"category": "memory"}
)
# → 상위 5개 유사 문서 검색
# 4. SQL Search (Solar API 호출 없음)
sql_results = sqlite.execute(
"SELECT * FROM documents WHERE content LIKE '%메모리%' LIMIT 5"
)
# 5. 결과 병합
all_docs = merge_results(vector_results, sql_results)
# → 최종 5개 문서 선택
# 6. 컨텍스트 생성
context = format_documents(all_docs)
# → "문서1 내용...\n\n문서2 내용..."
# 7. LLM 응답 생성 (Solar LLM API 호출 #2)
final_prompt = build_prompt(user_query, context, conversation_history)
response = solar_llm.invoke(final_prompt)
# → 최종 답변 생성
# 8. 결과 반환
return {
"answer": response.content,
"sources": [doc.metadata for doc in all_docs],
"tokens_used": response.usage
}
┌─────────────────────────────────────────────────────┐
│ 사용자 인터페이스 │
│ (Streamlit - demo_with_memory.py) │
└─────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ RAG 오케스트레이터 │
│ (main.py / main_simple.py) │
└─────────┬───────────────────┬───────────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────────────┐
│ 검색 시스템 │ │ 생성 시스템 │
│ (retriever.py) │ │ (Solar LLM via llm.py) │
└────┬──────┬──────┘ └──────────────────────────────┘
│ │ ▲
▼ ▼ │
┌────────┐ ┌────────┐ │
│ChromaDB│ │SQLite │ │
│ │ │ DB │ ─────────────┘
│ Vector │ │Keyword │ Context
│ Store │ │Search │
└────────┘ └────────┘
Solar API 호출 지점:
① 문서 인덱싱 시: Solar Embedding
② 질문 벡터화 시: Solar Embedding
③ 답변 생성 시: Solar LLM
④ SQL 쿼리 생성 시: Solar LLM
# llm.py (라인 53-82)
def get_embeddings(model: str = "solar-embedding-1-large") -> Embeddings:
api_key = os.getenv("UPSTAGE_API_KEY")
return UpstageEmbeddings(
api_key=api_key,
model=model
)
# vector_database.py에서 사용
embeddings = get_embeddings()
vectorstore = Chroma(
collection_name="langchain_docs",
embedding_function=embeddings,
persist_directory="./data/chroma_db"
)
# llm.py (라인 15-50)
def get_llm(model: str = "solar-pro", temperature: float = 0.1):
return ChatUpstage(
api_key=api_key,
model=model,
temperature=temperature,
max_tokens=2000
)
# demo_with_memory.py에서 사용
llm = get_llm()
response = llm.invoke(prompt)
# retriever.py (라인 151-195)
def search(self, query: str, k: int = 5, mode: str = "hybrid"):
if mode == "hybrid":
# Vector + SQL 병합
vector_results = self.vector_search(query, k)
sql_results = self.sql_search(query, k)
# 점수 기반 재정렬
all_results = vector_results + sql_results
seen = set()
unique_results = []
for result in sorted(all_results, key=lambda x: x.score, reverse=True):
doc_id = result.document.metadata.get("doc_id")
if doc_id not in seen:
seen.add(doc_id)
unique_results.append(result)
return [r.document for r in unique_results[:k]]
사용자 질문 처리
SQL 쿼리 탭 사용 시
# 제안: 질문-답변 캐싱
from functools import lru_cache
@lru_cache(maxsize=100)
def get_cached_response(query_hash: str):
# 자주 묻는 질문은 캐시에서 반환
return cached_response
# 제안: 임베딩 벡터 캐싱
embedding_cache = {}
def get_query_embedding(query: str):
if query in embedding_cache:
return embedding_cache[query]
embedding = solar_embedding.embed_query(query)
embedding_cache[query] = embedding
return embedding
# 제안: 질문 복잡도에 따른 동적 k값
def get_dynamic_k(query: str) -> int:
if is_complex_query(query):
return 10 # 복잡한 질문은 더 많은 문서 참조
return 5 # 단순 질문은 적은 문서로 충분
# 제안: Vector/SQL 검색 병렬 처리
import asyncio
async def parallel_search(query: str):
vector_task = asyncio.create_task(vector_search_async(query))
sql_task = asyncio.create_task(sql_search_async(query))
vector_results, sql_results = await asyncio.gather(vector_task, sql_task)
return merge_results(vector_results, sql_results)
# 제안: 컨텍스트 압축
def compress_context(documents: List[Document], max_tokens: int = 1500):
# 중요도 기반 문서 요약
compressed = []
token_count = 0
for doc in sorted(documents, key=lambda x: x.score, reverse=True):
doc_tokens = count_tokens(doc.content)
if token_count + doc_tokens <= max_tokens:
compressed.append(doc)
token_count += doc_tokens
else:
# 문서 요약 후 추가
summary = summarize(doc.content, max_tokens - token_count)
compressed.append(summary)
break
return compressed