LangGraph로 Agentic RAG 구현하기
LangGraph로 Agentic RAG 구현하기 (tutorials-build-essentials-agentic-rag-langgraph)
| 소요 시간: 45분 | 난이도: 중급(Intermediate) |
|---|
전통적인 RAG(검색 증강 생성, Retrieval-Augmented Generation) 시스템은 단순한 경로를 따라요. 쿼리 → 검색 → 생성. 물론 많은 시나리오에서 잘 동작하죠. 하지만 솔직히 말해서, 여러 단계가 필요하거나 서로 다른 유형의 정보를 끌어 모아야 하는 복잡한 쿼리를 다룰 때는 이 선형적인 접근 방식이 종종 어려움을 겪어요.
Agentic RAG는 한 단계 더 나아가요. 여러 번의 검색 단계를 오케스트레이션하고, 필요한 정보를 어떻게 수집하고 사용할지 스스로 판단하는 AI 에이전트를 도입하죠. 이렇게 생각해 볼게요. Agentic RAG 워크플로우에서 RAG는 훨씬 더 크고 다재다능한 도구 키트 속의 강력한 도구 하나일 뿐이에요.
LangGraph의 튼튼한 상태 관리와 Qdrant의 최첨단 벡터 검색을 결합해서, 단순히 질문에 답하는 것을 넘어 복잡한 다단계 정보 검색 작업을 유려하게 처리하는 시스템을 만들어 볼 거예요.
무엇을 만들까
LangGraph를 사용해서 Hugging Face와 Transformers 문서에 대한 질문에 답하는 AI 에이전트를 만들 거예요. 우리 AI 에이전트의 핵심에는 LangGraph가 있는데, 마치 오케스트라의 지휘자처럼 동작해요. 언제 정보를 검색하고, 언제 웹 검색을 수행하고, 언제 응답을 생성할지 결정하면서 여러 컴포넌트 사이의 흐름을 지휘하죠.
컴포넌트는 두 개의 Qdrant 벡터 저장소와 Brave 웹 검색 엔진이에요. 하지만 우리 에이전트는 단순히 한 경로만 맹목적으로 따르지 않아요. 각 쿼리를 평가해서 첫 번째 벡터 저장소에 접근할지, 두 번째에 접근할지, 아니면 웹을 검색할지 결정하죠.
이런 선택적 접근 방식 덕분에, 전통적인 RAG처럼 매번 같은 검색 과정에 갇히지 않고 작업에 가장 적합한 데이터 소스를 고르는 유연성이 생겨요. 이 튜토리얼에서 쿼리 정제(query refinement)를 깊이 다루지는 않지만, 여기서 배우는 개념은 나중에 그 기능을 추가하기 위한 튼튼한 기초가 돼요.
워크플로우

| 단계 | 설명 |
|---|---|
| 1. 사용자 입력 | 챗봇이나 웹 폼 같은 인터페이스를 통해 쿼리나 요청을 입력하는 것부터 시작해요. 이 쿼리는 작업의 두뇌인 AI Agent로 바로 전달돼요. |
| 2. AI Agent가 쿼리 처리 | AI Agent는 여러분의 쿼리를 분석해서 무엇을 묻는지, 어떤 도구나 데이터 소스가 질문에 가장 잘 답할 수 있는지 파악해요. |
| 3. 도구 선택 | 분석을 바탕으로 AI Agent는 작업에 맞는 도구를 골라요. 데이터는 두 개의 벡터 데이터베이스에 나뉘어 있고, 쿼리에 따라 적절한 것을 선택해요. 실시간 또는 외부 웹 데이터가 필요한 쿼리에는 BraveSearchAPI로 구동되는 웹 검색 도구를 사용해요. |
| 4. 쿼리 실행 | AI Agent는 선택한 도구를 작동시켜요: - RAG Tool 1이 벡터 데이터베이스 1을 조회해요. - RAG Tool 2가 벡터 데이터베이스 2를 조회해요. - 웹 검색 도구가 검색 API로 인터넷을 뒤져요. |
| 5. 데이터 검색 | 결과가 들어와요: - 벡터 데이터베이스 1과 2가 쿼리에 가장 관련 있는 문서를 돌려줘요. - 웹 검색 도구가 최신 또는 외부 정보를 제공해요. |
| 6. 응답 생성 | 텍스트 생성 모델(예: GPT)로 AI Agent가 쿼리에 맞춘 상세하고 정확한 응답을 만든다. |
| 7. 사용자 응답 | 다듬어진 응답이 인터페이스를 통해 다시 전달돼요. 바로 사용할 수 있게요. |
스택
이 아키텍처는 효율적인 Agentic RAG 워크플로우를 구동하기 위해 최첨단 도구를 활용해요. 그 컴포넌트와 필요한 기술을 간단히 살펴볼게요:
- AI Agent: 시스템의 총괄자로, 쿼리를 파싱하고 적절한 도구를 고르고 응답을 통합해요. LangGraph가 원활하게 관리하는 OpenAI의 gpt-4o를 추론 엔진으로 사용할 거예요.
- 임베딩(Embedding): OpenAI의 text-embedding-3-small 모델로 쿼리를 벡터 임베딩으로 변환해요.
- 벡터 데이터베이스: 임베딩을 저장하고 유사도 검색에 쓰는데, Qdrant가 우리가 고른 데이터베이스예요.
- LLM: OpenAI의 gpt-4o로 응답을 생성해서, 답변이 정확하고 맥락에 근거하도록 보장해요.
- 검색 도구: RAG의 능력을 확장하려고 BraveSearchAPI로 구동되는 웹 검색 컴포넌트를 추가했어요. 실시간 및 외부 데이터 검색에 완벽하죠.
- 워크플로우 관리: 전체 오케스트레이션과 의사결정 흐름을 LangGraph로 구축해서, 복잡한 워크플로우를 처리하는 데 필요한 유연성과 지능을 제공해요.
이 시스템을 처음부터 구축할 준비가 됐나요? 시작해 볼게요.
구현
에이전트를 만들기 전에 모든 것을 설정해 볼게요.
임포트 (Imports)
필요한 핵심 임포트 목록이에요:
import os
import json
from typing import Annotated, TypedDict
from dotenv import load_dotenv
from langchain.embeddings import OpenAIEmbeddings
from langgraph import StateGraph, tool, ToolNode, ToolMessage
from langchain.document_loaders import HuggingFaceDatasetLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.llms import ChatOpenAI
from qdrant_client import QdrantClient
from qdrant_client.http.models import VectorParams
from brave_search import BraveSearch
Qdrant 벡터 데이터베이스 설정
문서 임베딩을 위한 벡터 저장소로 Qdrant Cloud를 사용할 거예요. 설정 방법은 다음과 같아요:
| 단계 | 설명 |
|---|---|
| 1. 계정 만들기 | 아직 없으면 Qdrant Cloud에 가서 가입하세요. |
| 2. 클러스터 설정 | 계정에 로그인해서 대시보드에서 Create New Cluster 버튼을 찾으세요. 안내에 따라 설정하세요: - 원하는 리전을 선택하세요. - 테스트용으로 무료 티어를 선택하세요. |
| 3. 정보 안전하게 보관 | 클러스터가 준비되면 다음 세부 정보를 기록하세요: - 클러스터 URL(예: https://xxx-xxx-xxx.aws.cloud.qdrant.io) - API 키 |
이 정보는 나중을 위해 안전하게 저장해 두세요!
OpenAI API 구성
여러분의 OpenAI API 키가 임베딩 생성과 언어 모델 상호작용을 모두 구동해요. OpenAI 플랫폼에 방문해서 계정을 만들고, 대시보드의 API 섹션에서 새 API 키를 만들어요. 임베딩에는 text-embedding-3-small 모델을, 언어 모델로는 GPT-4를 사용할 거예요.
Brave Search
검색 기능을 강화하려고 Brave Search를 통합할 거예요. Brave API에 방문해서 API 접근 요청 절차를 완료하면 API 키를 얻을 수 있어요. 이 키가 에이전트의 웹 검색 기능을 활성화해요.
보안을 위해 모든 API 키는 .env 파일에 저장하세요.
OPENAI_API_KEY = <your-openai-api-key>
QDRANT_KEY = <your-qdrant-api-key>
QDRANT_URL = <your-qdrant-url>
BRAVE_API_KEY = <your-brave-api-key>
그런 다음 환경 변수를 로드하세요:
load_dotenv()
qdrant_key = os.getenv("QDRANT_KEY")
qdrant_url = os.getenv("QDRANT_URL")
brave_key = os.getenv("BRAVE_API_KEY")
문서 처리
에이전트를 만들기 전에 문서를 처리하고 저장해야 해요. Hugging Face에서 두 개의 데이터셋을 사용할 거예요. 일반 문서와 Transformers 전용 문서예요.
문서 전처리 함수는 이렇게 생겼어요:
def preprocess_dataset(docs_list):
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=700,
chunk_overlap=50,
disallowed_special=(),
)
doc_splits = text_splitter.split_documents(docs_list)
return doc_splits
이 함수는 문서를 관리 가능한 청크로 나누고, 오버랩(overlap)을 통해 청크 경계에서 중요한 맥락이 보존되도록 해요. HuggingFaceDatasetLoader를 사용해서 데이터셋을 Hugging Face 문서로 불러올 거예요.
hugging_face_doc = HuggingFaceDatasetLoader("m-ric/huggingface_doc", "text")
transformers_doc = HuggingFaceDatasetLoader("m-ric/transformers_documentation_en", "text")
이 데모에서는 데이터셋에서 처음 50개의 문서를 골라 처리 함수에 넘겨요.
hf_splits = preprocess_dataset(hugging_face_doc.load()[:number_of_docs])
transformer_splits = preprocess_dataset(transformers_doc.load()[:number_of_docs])
분할이 준비됐어요. 이제 Qdrant에 저장할 컬렉션을 만들어 볼게요.
상태 정의하기
LangGraph에서 state(상태) 란 프로세스나 일련의 작업이 실행되는 동안 특정 시점에 저장되고 유지되는 데이터나 정보를 말해요. 상태는 시스템이 작업 흐름을 관리하고 제어하기 위해 추적해야 하는 중간 또는 최종 결과를 담아요.
LangGraph는 상태 기반 시스템으로 동작해요. 상태를 이렇게 정의해요:
class State(TypedDict):
messages: Annotated[list, add_messages]
도구를 만들어 볼게요.
도구 만들기
우리 에이전트는 세 가지 강력한 도구를 갖추고 있어요:
- Hugging Face 문서 검색기 (Retriever)
- Transformers 문서 검색기
- 웹 검색 도구
문서와 컬렉션 이름을 받아서 검색기(retriever)를 돌려주는 함수부터 정의해 볼게요. 쿼리는 OpenAIEmbeddings를 사용해 벡터로 변환돼요.
def create_retriever(collection_name, doc_splits):
vectorstore = QdrantVectorStore.from_documents(
doc_splits,
OpenAIEmbeddings(model="text-embedding-3-small"),
url=qdrant_url,
api_key=qdrant_key,
collection_name=collection_name,
)
return vectorstore.as_retriever()
Hugging Face 문서 검색기와 Transformers 문서 검색기 모두 이 함수를 사용해요. 이 설정 덕분에 각각에 대해 별도의 도구를 아주 간단하게 만들 수 있어요.
hf_retriever_tool = create_retriever_tool(
hf_retriever,
"retriever_hugging_face_documentation",
"Search and return information about hugging face documentation, it includes the guide and Python code.",
)
transformer_retriever_tool = create_retriever_tool(
transformer_retriever,
"retriever_transformer",
"Search and return information specifically about transformers library",
)
웹 검색을 위해서는 Brave Search를 사용하는 단순하지만 효과적인 도구를 만들어요:
@tool("web_search_tool")
def search_tool(query):
search = BraveSearch.from_api_key(
api_key=brave_key,
search_kwargs={"count": 3},
)
return search.run(query)
search_tool 함수는 BraveSearch API로 검색을 수행해요. 쿼리를 받아 API 키로 상위 3개의 검색 결과를 가져오고 결과를 돌려줘요.
다음으로, 도구를 설정하고 언어 모델과 통합할 거예요:
tools = [hf_retriever_tool, transformer_retriever_tool, search_tool]
tool_node = ToolNode(tools=tools)
llm = ChatOpenAI(model="gpt-4o", temperature=0)
llm_with_tools = llm.bind_tools(tools)
여기서 ToolNode 클래스가 도구를 처리하고 오케스트레이션해요:
class ToolNode:
def __init__(self, tools: list) -> None:
self.tools_by_name = {tool.name: tool for tool in tools}
def __call__(self, inputs: dict):
if messages := inputs.get("messages", []):
message = messages[-1]
else:
raise ValueError("No message found in input")
outputs = []
for tool_call in message.tool_calls:
tool_result = self.tools_by_name[tool_call["name"]].invoke(tool_call["args"])
outputs.append(
ToolMessage(
content=json.dumps(tool_result),
name=tool_call["name"],
tool_call_id=tool_call["id"],
)
)
return {"messages": outputs}
ToolNode 클래스는 도구 목록을 초기화하고 도구 이름을 해당 함수에 매핑해서 도구 실행을 처리해요. 입력 딕셔너리를 처리하고, 마지막 메시지를 추출하며, Anthropic, OpenAI 등의 LLM 도구 호출 기능 제공자로부터의 tool_calls를 확인하죠.
라우팅과 의사결정
우리 에이전트는 언제 도구를 사용하고 언제 순환을 끝낼지 결정해야 해요. 이 결정은 라우팅 함수가 관리해요:
def route(state: State):
if isinstance(state, list):
ai_message = state[-1]
elif messages := state.get("messages", []):
ai_message = messages[-1]
else:
raise ValueError(f"No messages found in input state to tool_edge: {state}")
if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
return "tools"
return END
그래프로 모두 합치기
마지막으로, 모든 것을 묶는 그래프를 만들어 볼게요:
graph_builder = StateGraph(State)
graph_builder.add_node("agent", agent)
graph_builder.add_node("tools", tool_node)
graph_builder.add_conditional_edges(
"agent",
route,
{"tools": "tools", END: END},
)
graph_builder.add_edge("tools", "agent")
graph_builder.add_edge(START, "agent")
그래프는 이렇게 생겼어요:

Fig. 3: Agentic RAG with LangGraph
에이전트 실행하기
모든 게 준비되면, 간단한 함수로 에이전트를 실행할 수 있어요:
def run_agent(user_input: str):
for event in graph.stream({"messages": [("user", user_input)]}):
for value in event.values():
print("Assistant:", value["messages"][-1].content)
이제 Hugging Face와 Transformers에 대해 질문할 준비가 됐어요! 우리 에이전트는 필요할 때 문서의 정보와 웹 검색 결과를 지능적으로 결합해요.
예를 들어, 이렇게 물어볼 수 있어요:
In the Transformers library, are there any multilingual models?
에이전트는 Transformers 문서를 파고들어 다국어 모델에 관한 관련 세부 정보를 추출하고, 명확하고 포괄적인 답변을 전달해요.
응답은 대략 이렇게 보일 거예요:
Yes, the Transformers library includes several multilingual models. Here are some examples:
BERT Multilingual: Models like `bert-base-multilingual-uncased` can be used just like monolingual models.
XLM (Cross-lingual Language Model): Models like `xlm-mlm-ende-1024` (English-German), `xlm-mlm-enfr-1024` (English-French), and others use language embeddings to specify the language used at inference.
M2M100: Models like `facebook/m2m100_418M` and `facebook/m2m100_1.2B` are used for multilingual translation.
MBart: Models like `facebook/mbart-large-50-one-to-many-mmt` and `facebook/mbart-large-50-many-to-many-mmt` are used for multilingual machine translation across 50 languages.
These models are designed to handle multiple languages and can be used for tasks like translation, classification, and more.
결론
Agentic RAG를 성공적으로 구현했어요. 하지만 이건 시작일 뿐이에요. 시스템을 한 단계 더 끌어올리기 위해 탐험할 수 있는 것이 아주 많아요.
Agentic RAG는 기업이 데이터 소스를 AI와 연결하는 방식을 변화시키고 있어요. 더 똑똑하고 더 역동적인 상호작용을 가능하게 하죠. 이 튜토리얼에서 여러분은 LangGraph, Qdrant, 웹 검색의 힘을 하나의 매끄러운 워크플로우로 결합한 Agentic RAG 시스템을 구축하는 방법을 배웠어요.
이 시스템은 Hugging Face와 Transformers 문서에서 관련 정보를 검색하는 데 그치지 않아요. 필요할 때는 똑똑하게 웹 검색으로 폴백해서, 어떤 쿼리도 답 없이 남지 않도록 보장하죠. 벡터 데이터베이스의 백본으로 Qdrant를 사용하면, 방대한 데이터셋에서도 정밀한 정보를 검색하는 데 뛰어난 빠르고 확장 가능한 의미 검색을 얻을 수 있어요.
이 접근 방식의 잠재력을 온전히 깨닫기 위해, 이런 개념을 여러분 자신의 프로젝트에 적용해 보는 건 어떨까요? 우리가 공유한 템플릿을 여러분의 특정 사용 사례에 맞게 커스터마이즈해서, 비즈니스 요구에 맞는 Agentic RAG의 잠재력을 온전히 열어 보세요. 가능성은 무한해요.