DeepRAG는 검색 쿼리를 원자 명제(atomic proposition) 단위로 분해하고, 트리 기반 구조에서 각 노드마다 “검색할 것인가, 더 분해할 것인가”를 결정하는 RAG 아키텍처입니다. 복잡한 질문을 가장 작은 사실 단위까지 분해한 뒤, 각 단위에 대해 정밀하게 검색하여 답변의 정확도를 높입니다.
from langchain.chat_models import init_chat_modelfrom langchain_openai import OpenAIEmbeddingsfrom langchain_chroma import Chromafrom langchain_core.prompts import ChatPromptTemplatefrom langchain_core.output_parsers import StrOutputParserllm = init_chat_model("gpt-4o-mini", temperature=0)embeddings = OpenAIEmbeddings()vectorstore = Chroma(collection_name="documents", embedding_function=embeddings)retriever = vectorstore.as_retriever(search_kwargs={"k": 3})MAX_DECOMPOSITION_DEPTH = 3def decompose(state: DeepRAGState) -> DeepRAGState: """질문을 원자 명제 수준의 하위 질문으로 분해합니다.""" question = state["question"] pending = state.get("pending_queries", []) # 처음 호출 시 원본 질문을 분해 target = pending[0] if pending else question prompt = ChatPromptTemplate.from_messages([ ("system", ( "복잡한 질문을 답변에 필요한 원자적(atomic) 하위 질문으로 분해하세요.\n" "원자적 질문이란 하나의 사실만을 묻는 가장 작은 단위의 질문입니다.\n" "각 하위 질문을 한 줄에 하나씩, 번호 없이 출력하세요.\n" "이미 충분히 원자적이면 원래 질문을 그대로 출력하세요." )), ("human", "{question}"), ]) chain = prompt | llm | StrOutputParser() result = chain.invoke({"question": target}) sub_queries = [q.strip() for q in result.strip().split("\n") if q.strip()] # pending에서 현재 처리한 쿼리 제거 remaining_pending = pending[1:] if pending else [] return {"pending_queries": sub_queries + remaining_pending}
Copy
def decide_action(state: DeepRAGState) -> DeepRAGState: """각 pending 쿼리가 원자적인지 판단하여 검색 또는 추가 분해를 결정합니다.""" pending = state.get("pending_queries", []) atomic = state.get("atomic_queries", []) if not pending: return state current_query = pending[0] prompt = ChatPromptTemplate.from_messages([ ("system", ( "다음 질문이 원자적(atomic)인지 판단하세요.\n" "원자적 질문: 하나의 사실만을 묻는 단순한 질문 (예: 'X의 저자는?')\n" "복합 질문: 여러 사실을 동시에 묻는 질문 (예: 'X와 Y의 차이점은?')\n" "'atomic' 또는 'complex'만 출력하세요." )), ("human", "{question}"), ]) chain = prompt | llm | StrOutputParser() result = chain.invoke({"question": current_query}) if "atomic" in result.strip().lower(): # 원자적이면 검색 대상에 추가 updated_atomic = list(atomic) + [current_query] return { "atomic_queries": updated_atomic, "pending_queries": pending[1:], } else: # 복합이면 pending에 유지 (다음 decompose에서 처리) return state
Copy
def retrieve_atomic(state: DeepRAGState) -> DeepRAGState: """원자 쿼리에 대해 검색을 수행합니다.""" atomic_queries = state.get("atomic_queries", []) existing_results = state.get("retrieval_results", []) new_results = [] for query in atomic_queries: documents = retriever.invoke(query) context = "\n".join(doc.page_content for doc in documents) new_results.append(f"[{query}]\n{context}") return { "retrieval_results": existing_results + new_results, "atomic_queries": [], # 처리 완료 }
Copy
def synthesize(state: DeepRAGState) -> DeepRAGState: """모든 원자 검색 결과를 종합하여 최종 답변을 생성합니다.""" question = state["question"] results = state.get("retrieval_results", []) context = "\n\n".join(results) prompt = ChatPromptTemplate.from_messages([ ("system", ( "다음은 질문을 원자 단위로 분해하여 각각 검색한 결과입니다.\n" "모든 결과를 종합하여 원래 질문에 답변하세요.\n\n" "{context}" )), ("human", "{question}"), ]) chain = prompt | llm | StrOutputParser() answer = chain.invoke({"context": context, "question": question}) return {"answer": answer}
from typing import Literaldef route_action(state: DeepRAGState) -> Literal["decompose", "retrieve", "synthesize"]: """pending 쿼리 상태에 따라 다음 행동을 결정합니다.""" pending = state.get("pending_queries", []) atomic = state.get("atomic_queries", []) # 검색 대상이 있으면 먼저 검색 if atomic: return "retrieve" # 아직 처리할 쿼리가 있으면 결정 계속 if pending: return "decompose" # 모든 처리 완료 return "synthesize"def route_after_decide(state: DeepRAGState) -> Literal["decide", "decompose", "retrieve", "synthesize"]: """decide_action 후 다음 단계를 결정합니다.""" pending = state.get("pending_queries", []) atomic = state.get("atomic_queries", []) if atomic: return "retrieve" if pending: return "decide" return "synthesize"
result = app.invoke({ "question": "BERT와 GPT의 사전학습 방식 차이를 설명하고, 각각 어떤 태스크에 적합한지 비교하라", "pending_queries": [], "atomic_queries": [], "retrieval_results": [],})print(result["answer"])
재귀적 분해는 무한 루프의 위험이 있습니다. 위 구현에서는 decide_action이 원자적 판단을 수행하지만, 실제 프로덕션에서는 분해 깊이 제한(MAX_DECOMPOSITION_DEPTH)을 decompose 함수 내에서 엄격히 적용하여 과도한 분해를 방지해야 합니다.