Query VendingMachine 프로젝트에 LangChain을 도입하여 코드 구조를 개선하고 RAG(Retrieval-Augmented Generation) 파이프라인을 구현했습니다.
목표: 모듈별 책임 분리
프로젝트 구조
├── config/
│ └── llm_config.py # LLM 설정
├── retrievers/
│ └── db_retriever.py # 벡터 검색 Retriever
├── prompts/
│ └── sql_generation_prompt.py # 프롬프트 템플릿
├── chains/
│ ├── sql_generation_chain.py # 기본 LLM 체인
│ └── text_to_sql_chain.py # 최종 RAG 체인
├── utils/
│ ├── db_utils.py # DB 유틸리티
│ └── logging_utils.py # 로깅 유틸리티
└── main.py # Streamlit 앱 (리팩토링)
이전: 프롬프트가 main.py 내에 하드코딩
# 기존: main.py 내 하드코딩
system_prompt = """You are an expert SQL generator...
"""이후: 프롬프트 템플릿 분리
# 신규: prompts/sql_generation_prompt.py
from langchain.prompts import ChatPromptTemplate
def get_sql_generation_prompt():
return ChatPromptTemplate.from_messages([
("system", SYSTEM_PROMPT),
("user", USER_PROMPT_TEMPLATE),
])장점:
- 프롬프트 변경 시
main.py수정 불필요 - 프롬프트 버전 관리 용이
- A/B 테스트를 위한 변형 프롬프트 관리 간편
이전: 직접 OpenAI API 호출
# 기존
from openai import OpenAI
client = OpenAI(api_key=API_KEY)
resp = client.chat.completions.create(...)이후: LangChain ChatOpenAI 사용
# 신규: config/llm_config.py
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-5-mini")장점:
- LLM 변경 용이 (OpenAI → Anthropic, Google 등)
- LCEL을 통한 선언적 체인 구성
- 일관된 인터페이스
이전: 순수 함수 기반 검색
# 기존
def search_docs(query: str, limit: int = 10):
query_emb = get_embedding(query)
# ... 검색 로직 ...
return results이후: LangChain Retriever 인터페이스
# 신규: retrievers/db_retriever.py
class DVDRentalRetriever(BaseRetriever):
def _get_relevant_documents(self, query: str) -> List[Document]:
# ... 검색 로직 ...
return documents장점:
- LangChain 에코시스템 표준 준수
- 하이브리드 검색 추가 용이
- 다른 벡터 DB로 전환 시 코드 변경 최소화
이전: 복잡한 함수 중첩
# 기존
results = search_docs(natural_query)
sql = generate_sql(natural_query) # 벡터 검색 재호출!
query = clean_sql_output(sql)
rows = run_query(query)이후: LCEL 선언적 체인
# 신규: chains/text_to_sql_chain.py
text_to_sql_chain = (
RunnableLambda(get_context_and_primary_table)
| prompt
| llm
| StrOutputParser()
| RunnableLambda(clean_sql_output)
)
# 사용
sql = text_to_sql_chain.invoke(question)장점:
- 파이프라인 명확성 향상
- 디버깅 및 모니터링 용이
- 향후 재시도, 회로차단 등 기능 추가 간편
| 측면 | 기존 방식 | LangChain 방식 |
|---|---|---|
| 프롬프트 관리 | 하드코딩 | PromptTemplate |
| LLM 호출 | OpenAI SDK 직접 사용 | ChatOpenAI 래퍼 |
| 벡터 검색 | 순수 함수 | BaseRetriever 상속 |
| 파이프라인 | 함수 중첩 | LCEL (선언적) |
| LLM 변경 | 코드 수정 필요 | 설정만 변경 |
| 캐싱 | 수동 구현 필요 | 내장 지원 |
| 모니터링 | 수동 로깅 | Callbacks 지원 |
# 처음 실행시 (초기 테이블 벡터 임베딩 생성)
INIT_TABLE_DOCS=1 streamlit run main.py
# 이후 실행
streamlit run main.pyfrom chains.text_to_sql_chain import invoke_text_to_sql_chain
# 자연어 질문 → SQL 변환
sql = invoke_text_to_sql_chain("영화 제목과 개봉년도를 나열해줘")
print(sql) # SELECT title, release_year FROM film;from prompts.sql_generation_prompt import get_sql_generation_prompt
# 프롬프트 수정
prompt = get_sql_generation_prompt()
# → prompts/sql_generation_prompt.py 파일 직접 수정# config/llm_config.py 수정
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-3-sonnet", temperature=0.2)# retrievers/db_retriever.py 에서 limit 조정
retriever = get_dvdrental_retriever(limit=5) # 기본값: 10# utils/logging_utils.py 에서 로깅 커스터마이징
from langchain_core.callbacks import StdOutCallbackHandler
# 체인에 콜백 추가
chain.invoke(input, config={"callbacks": [StdOutCallbackHandler()]})- Callbacks 통합: LangChain Callbacks으로 전체 파이프라인 모니터링
- 캐싱: SQLAlchemy 기반 캐싱 구현
- 메모리 관리: ConversationBufferMemory 추가로 대화형 기능
- 고급 체인 기능:
- 재시도 (Retry) 로직
- 회로차단 (Fallback) 패턴
- 동적 프롬프트 선택
A: config/llm_config.py에서 모델명을 수정하세요.
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.2)A:
- 임베딩을 재생성합니다:
INIT_TABLE_DOCS=1 streamlit run main.py - 프롬프트를 조정합니다:
prompts/sql_generation_prompt.py
A:
- 리트리버 제한을 줄입니다:
limit=5 - 모델을 변경합니다:
gpt-3.5-turbo(더 빠름)