-
Retriever를 에이전트 도구로 — RAG 패턴 구현IT 2026. 6. 24. 22:00
LLM이 틀린 답을 내놓는 이유는 크게 두 가지다. 첫째, 학습 데이터에 없는 내용(특정 도메인 지식, 사내 문서, 개인 기록). 둘째, 학습한 내용과 현실 사이의 시간 차. 두 경우 모두 "모델이 모르는 내용"이라는 공통점이 있다. 이때 쓰는 패턴이 RAG(Retrieval-Augmented Generation) — 검색해서 가져온 문서를 LLM 프롬프트에 끼워 넣어 답변 품질을 높이는 방식이다.
이 글에서는 영화 정보 파일을 벡터 데이터베이스에 저장하고, 그 검색기를 에이전트 도구로 등록하는 전체 파이프라인을 순서대로 살펴본다.
RAG 파이프라인 전체 흐름
문서가 에이전트 도구로 동작하기까지 여섯 단계를 거친다.
RAG 파이프라인 전체 구조. 파이프라인은 "준비 단계"와 "실행 단계"로 나뉜다. 준비 단계(파일 로드 → 청크 분할 → 벡터 변환 → 저장)는 한 번만 실행한다. 실행 단계(에이전트가 도구를 호출 → retriever가 유사 청크 검색 → LLM이 해당 내용을 바탕으로 답변)는 매 질문마다 반복된다. 세 번째 단계가 핵심 — OpenAIEmbeddings가 텍스트를 숫자 배열(벡터)로 변환한다. 이 변환이 있어야 "의미가 비슷한 텍스트"를 수치적으로 찾을 수 있다. "스릴러 영화"를 검색해도 "서스펜스"가 담긴 문서를 찾아오는 게 임베딩 덕분이다.
청크 분할이 중요한 이유
파이프라인에서 가장 조정이 필요한 부분은
RecursiveCharacterTextSplitter다. 두 파라미터가 핵심이다.- chunk_size: 각 청크의 최대 글자 수. 256이면 대략 두세 문장 분량이다. 너무 크면 검색 정밀도가 떨어지고, 너무 작으면 문맥이 잘려 LLM이 의미를 파악하기 어렵다.
- chunk_overlap: 앞 청크와 겹치는 글자 수. 64를 주면 앞 청크의 마지막 64자가 다음 청크의 앞에 그대로 온다. 문장 경계에서 내용이 잘릴 때 문맥을 보존한다.
주제별 컬렉션 분리
문서를 하나의 벡터 데이터베이스에 몽땅 넣으면 어떻게 될까? "감독"을 검색했는데 "감독 스타일 분석"(비평 문서)이 아니라 "감독 인터뷰"(제작 비화 문서)가 걸릴 수 있다. 검색 결과가 뒤섞이면 LLM이 잘못된 근거로 답변한다.
다중 컬렉션 구조. 컬렉션을 주제별로 나누면 각 retriever가 담당 영역 안에서만 검색하므로 정밀도가 올라간다. SQL 데이터베이스의 테이블 분리와 같은 개념이다. 트레이드오프: 컬렉션 수만큼 도구도 늘어난다. 에이전트가 어떤 도구를 쓸지 판단해야 하므로 도구 설명(docstring)을 명확하게 써야 한다.
Retriever를 에이전트 도구로 변환하기
retriever는 그 자체로는 에이전트가 호출할 수 없다.
@tool데코레이터로 감싸야 한다. 그 전에 먼저 retriever 자체를 만들어야 한다.from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma embedding = OpenAIEmbeddings() def make_retriever(file_path: str, collection_name: str): docs = TextLoader(file_path).load() chunks = RecursiveCharacterTextSplitter( chunk_size=256, chunk_overlap=64 ).split_documents(docs) store = Chroma.from_documents(chunks, embedding, collection_name=collection_name) return store.as_retriever(search_kwargs={"k": 3}) action_retriever = make_retriever("action_movies.txt", "action") director_retriever = make_retriever("director_info.txt", "director") romance_retriever = make_retriever("romance_movies.txt", "romance")retriever 생성 구조 풀이.
make_retriever는 세 가지 일을 한다. 첫째, 텍스트 파일을 256자 청크로 쪼갠다 —chunk_overlap=64로 앞뒤 청크가 64자씩 겹쳐 문장 경계에서 문맥이 잘리는 걸 방지한다. 둘째, OpenAI 임베딩으로 각 청크를 벡터로 변환해 Chroma에 저장한다 —collection_name이 다르면 같은 Chroma 인스턴스 안에서도 완전히 격리된 검색 공간이 된다. 셋째,k=3으로 검색 시 가장 유사한 청크 3개를 반환한다 — 단일 청크만 반환하면 관련 내용이 청크 경계에 잘렸을 때 누락된다. 이 함수를 장르·감독 파일별로 한 번씩 호출하면 독립된 retriever 세 개가 만들어진다. 이후 각 retriever를@tool로 감싸는 것이 다음 단계다.에이전트는 각 도구의 docstring을 읽고 "어떤 도구를 언제 써야 하는가"를 판단한다.
@tool def search_action_movies(query: str) -> str: """액션, 어드벤처 영화 정보를 검색한다. 예시: '최근 마블 영화 추천해줘', '총격전이 있는 영화는?'""" docs = action_retriever.invoke(query) return "\n\n".join([doc.page_content for doc in docs]) @tool def search_director_info(query: str) -> str: """감독의 연출 스타일, 필모그래피를 검색한다. 예시: '크리스토퍼 놀란 감독 작품은?', '봉준호 연출 특징은?'""" docs = director_retriever.invoke(query) return "\n\n".join([doc.page_content for doc in docs])핵심은 docstring이다.
search_action_movies의 docstring이 "액션, 어드벤처"면 사용자가 "총격전 있는 영화 추천해줘"라고 물을 때 에이전트가 이 도구를 고른다. docstring이 모호하거나 비슷한 내용이 여러 도구에 겹치면 에이전트가 잘못된 도구를 선택한다. 도구 docstring이 곧 에이전트의 라우팅 로직이다.복합 질문 처리 — 여러 도구를 동시에
"크리스토퍼 놀란 감독 액션 영화 중에 추천할 만한 게 있어?"처럼 두 카테고리에 걸친 질문은 어떻게 처리될까?
복합 질문 처리 흐름. LangChain 에이전트는 도구를 순차적으로도, 병렬로도 호출할 수 있다. "감독 관련은 search_director_info, 장르 관련은 search_action_movies"로 각각 판단해 두 도구를 호출하고, 두 결과를 합쳐 하나의 답변을 만든다. 단일 컬렉션 접근법으로는 이 정밀도를 내기 어렵다.
시스템 프롬프트 전략
RAG 패턴에서 시스템 프롬프트의 역할은 하나다: LLM이 자기 학습 지식 대신 도구 검색 결과에만 의존하도록 강제하는 것이다. 이 제약 없이는 LLM이 "대략 이런 영화가 있겠지"라고 추론해 일반적인 답변을 내놓을 수 있다.
system_prompt = """ 당신은 영화 추천 전문가입니다. 반드시 제공된 도구를 통해 정보를 검색한 후 답변하세요. 도구에서 찾은 내용에만 기반해 답변하고, 검색 결과에 없는 내용은 '데이터에서 찾을 수 없다'고 답하세요. """"찾을 수 없으면 없다고 하라"는 부분도 중요하다 — 이 지시 없이는 LLM이 검색 결과가 없을 때 추측으로 채운다. 특히 유명한 영화처럼 학습 데이터에서 "충분히" 답할 수 있어 보이는 경우에 도구 호출을 건너뛰는 경향이 생긴다.
왜 이 패턴이 필요한가
LLM이 학습 시점 이후에 개봉한 영화, 개인이 정리한 리뷰 데이터, 틈새 장르 정보를 알 방법은 없다. RAG 패턴은 이 한계를 정직하게 인정하는 설계다 — "모델이 모르면 검색해라."
@tool로 retriever를 등록하는 방식은 에이전트가 여러 지식 소스를 선택적으로 쓸 수 있게 하면서, 각 소스의 범위를 명확히 분리한다. 컬렉션 하나가 한 주제의 진실이 되는 구조다. 도구 docstring으로 라우팅 로직을 선언하고, 시스템 프롬프트로 의존 범위를 강제한다 — 이 두 제약이 맞물려야 RAG 에이전트가 예측 가능하게 동작한다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
LLM에 코드 실행 능력 붙이기 — PythonAstREPLTool (0) 2026.06.24 Pydantic Field로 LLM 출력 스키마를 제약하는 방법 (0) 2026.06.23 ProviderStrategy vs ToolStrategy — 구조화 출력 전략 선택 (0) 2026.06.23 LangChain ToolRuntime으로 런타임 컨텍스트 주입하기 (0) 2026.06.23 LangChain @tool 데코레이터의 3요소 — 에이전트가 도구를 이해하는 방법 (0) 2026.06.22 LangChain이 LLM을 다루는 방식: 추상화, 팩토리, 체이닝 (0) 2026.06.22 LangChain이 등장한 이유 — LLM 시대의 새로운 개발 패러다임 (0) 2026.06.22 JSON-RPC의 id는 누가 정하고 충돌하면 어떻게 되나 (0) 2026.06.21 서브에이전트를 200% 활용하는 노하우 — description부터 병렬 실행까지 (0) 2026.06.20 플러그인으로 서브에이전트 배포하기 — /plugin install부터 마켓플레이스까지 (0) 2026.06.19