-
RAG 에이전트 완전 조립 — create_agent부터 동작 추적까지IT 2026. 6. 29. 21:00
RAG 파이프라인을 구성하는 세 가지 재료가 있다. 문서를 저장한 벡터 DB에서 검색해 오는 리트리버, 그 리트리버를 LLM이 호출할 수 있는 형태로 포장한 도구(@tool), 그리고 질문의 의도에 따라 어떤 도구를 몇 번 쓸지 스스로 결정하는 에이전트. 이 세 가지를 하나로 묶는 접착제가
create_agent다.이 글에서는
create_agent로 에이전트를 조립하는 방법부터 시작해, 단일 카테고리 질문과 복합 카테고리 질문이 내부에서 어떻게 다르게 처리되는지, 그리고messages배열을 읽어 에이전트 동작을 추적하는 방법까지 순서대로 살펴본다.1. create_agent로 RAG 에이전트 조립
에이전트를 만드는 데 필요한 최소 재료는 세 가지다. LLM(
model), 도구 리스트(tools), 그리고 에이전트의 역할을 정의하는system_prompt.# 에이전트 역할·말투·행동 기준을 한 문단으로 정의 system_prompt = ( "당신은 영화 전문가입니다. " "사용자 질문에 검색된 문서를 기반으로 이해하기 쉽게 답변하세요." ) # 세 재료를 create_agent에 넘기면 ReAct 에이전트가 완성된다 agent = create_agent( model=model, # 추론을 담당하는 LLM tools=tools, # @tool로 감싼 검색 도구 리스트 system_prompt=system_prompt )create_agent는 내부적으로 LangGraph의 ReAct 그래프를 구성한다. 에이전트는 질문을 받으면 "어떤 도구를 써야 하는가"를 먼저 추론하고, 도구를 호출한 뒤 결과를 보고 최종 답변을 생성한다. 개발자가 이 분기 로직을 직접 짤 필요가 없다는 점이 핵심이다.system_prompt를 생략하면 에이전트가 답변의 톤·범위를 스스로 결정하므로, 영화 장르 특화 응용에서는 반드시 명시하는 것이 좋다.세 입력이 합쳐져 ReAct 에이전트 하나가 만들어지는 구조다.
system_prompt는 에이전트가 "어떤 맥락에서 답변해야 하는지"를 모든 추론 단계에 걸쳐 유지시켜 주는 역할을 한다. 빠뜨리면 에이전트가 범용 챗봇처럼 동작하므로 도메인 특화 응용에서는 필수다.2. system_prompt가 에이전트 동작에 미치는 영향
system_prompt가 있을 때와 없을 때 에이전트의 동작 방식이 어떻게 달라지는지 비교해 보면, 단순한 문구 차이 이상의 영향이 있음을 알 수 있다.두 경우를 나란히 보면 차이가 명확하다.
system_prompt가 없는 에이전트는 도구를 쓸지 말지, 어떤 형식으로 답할지를 매 질문마다 즉흥적으로 결정한다. 반면system_prompt가 있는 에이전트는 "검색된 문서를 기반으로 답변한다"는 기준이 고정되어 있어 응답 일관성이 높아진다. RAG 에이전트의 신뢰성은 검색 정확도만큼이나system_prompt의 명확성에 달려 있다.3. 단일 카테고리 질문 처리 흐름
"액션 영화 추천작을 알려줘"처럼 하나의 카테고리에만 관련된 질문은 에이전트가 도구를 한 번만 호출한다. 내부적으로는 네 개의 메시지가 순서대로 생성된다.
질문이 들어오면 에이전트는 우선 어떤 도구를 쓸지 결정하고 그 내용을
AIMessage의tool_calls필드에 기록한다. 그다음 실제 도구가 실행되어 검색 결과가ToolMessage로 돌아오고, 에이전트가 그 내용을 읽어 최종 답변을 생성한다. "액션 영화 추천작을 알려줘"처럼 단일 카테고리 질문에서는 이 사이클이 한 번만 돈다. 놓치기 쉬운 함정은, 첫 번째AIMessage가 최종 답변이 아니라는 점이다 —tool_calls가 있으면 아직 추론이 끝나지 않은 중간 단계다.4. 복합 카테고리 질문 처리 흐름 — 핵심 차별점
"드라마와 SF 영화를 비교해줘"처럼 두 카테고리를 동시에 묻는 질문에서 에이전트의 진가가 드러난다. 개발자가 분기 로직을 작성하지 않아도 에이전트가 스스로 두 도구를 각각 호출한다.
단일 질문 흐름과 비교하면
ToolMessage가 두 개 생기는 것이 핵심 차이다. 에이전트는 하나의AIMessage에서 두 도구 호출을 동시에 결정하고, 두 검색 결과가 모두 도착한 뒤에 비교 답변을 생성한다. 개발자가 "드라마 질문인지 SF 영화 질문인지"를 if-else로 분기할 필요가 없다. 단, 도구가 너무 많으면 에이전트가 불필요한 도구까지 호출하는 과잉 추론이 발생할 수 있으므로, 각 도구의description을 명확하게 작성해 호출 범위를 좁혀 주어야 한다.5. 동작 추적 — messages 배열 읽기
에이전트를
invoke하면 반환값의messages배열에 모든 실행 단계가 기록된다. 이 배열을 순서대로 읽으면 에이전트가 어떤 도구를 왜 선택했는지, 검색 결과가 무엇이었는지 전부 추적할 수 있다.for msg in response["messages"]: if hasattr(msg, 'tool_calls') and msg.tool_calls: print(f"도구 선택: {msg.tool_calls[0]['name']}") # AIMessage with tool_calls = 추론 중간 elif getattr(msg, 'type', '') == 'tool': print(f"검색 결과: {msg.content[:100]}...") print(f"최종 답변: {response['messages'][-1].content}") # 마지막 AIMessage = 완료messages배열에서 주의해야 할 메시지 타입은 세 가지다.tool_calls가 있는AIMessage는 에이전트가 도구를 호출하기로 결정한 순간을 기록한다.ToolMessage는 실제 도구가 실행되어 검색 결과를 돌려준 순간이다. 그리고 배열의 마지막AIMessage가 모든 검색 결과를 종합한 최종 답변이다. 디버깅 시 흔한 실수는 첫 번째AIMessage를 최종 답변으로 착각하는 것 —tool_calls유무를 먼저 확인해야 한다.배열의 각 메시지 타입별 역할을 한눈에 보면,
messages가 단순한 로그가 아니라 에이전트의 추론 과정 전체를 순서대로 담은 실행 기록임을 알 수 있다.tool_calls유무로 "아직 실행 중"과 "완료"를 구분하고, 타입이tool인 메시지들을 모으면 에이전트가 실제로 읽은 문서를 역추적할 수 있다.6. 전체 아키텍처 — 3개 레이어
지금까지 살펴본 구성 요소들이 실제로 어떻게 연결되는지 전체 그림으로 정리해 보자. 문서에서 에이전트 답변까지 세 개의 레이어를 거친다.
세 레이어는 역할이 명확하게 분리되어 있다. 레이어 1은 "어디서 검색할 것인가"를 정의한다 — 문서를 잘게 쪼개고 벡터로 저장하는 일회성 사전 작업이다. 레이어 2는 "어떻게 검색 결과를 에이전트에 전달할 것인가"를 정의한다 — 리트리버에
@tool을 씌워 에이전트가 호출할 수 있는 단위로 만든다. 레이어 3은 "누가 판단하는가"를 담당한다 —create_agent가 LLM에 도구 목록과 역할을 부여해 자율 추론 주체를 만든다. 레이어를 분리해 두면 문서를 추가하거나 도구 설명을 바꾸는 변경이 에이전트 코드에 영향을 주지 않는다는 장점이 있다. 세 레이어 중 어느 하나를 바꾸더라도 나머지는 그대로 유지된다.여기까지가 RAG 에이전트의 완전한 조립 과정이다. 핵심을 한 줄로 요약하면 — 리트리버를 도구로 포장하고, 도구를
create_agent에 넘기면, 에이전트가 질문의 복잡도에 맞게 도구 호출 횟수와 순서를 스스로 결정한다.messages배열은 그 모든 과정의 감사 로그다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
MCP는 왜 SSE를 골랐나 — HTTP 위에서 서버가 먼저 말하게 하는 법 (0) 2026.07.01 서브 에이전트를 @tool로 감싸는 멀티 에이전트 패턴 (0) 2026.06.30 @tool이 내부에서 하는 일 — Pydantic BaseModel이 LLM의 호출 인터페이스가 되는 과정 (0) 2026.06.30 Pydantic BaseModel이란 무엇인가 — 타입 힌트를 진짜 검증으로 바꾸는 도구 (0) 2026.06.29 LangGraph가 Annotated를 쓰는 이유 — 덮어쓰기 문제와 리듀서의 등장 (0) 2026.06.29 검색 결과를 에이전트 도구로 — build_context와 @tool 패턴 (0) 2026.06.28 RAG의 배경과 make_retriever — LLM이 모르는 문서를 검색하는 방법 (0) 2026.06.28 생성과 검증의 분리 — generator 노드와 validator 노드 설계 (0) 2026.06.28 LangGraph 자기 수정 패턴의 State 설계 — 루프를 위한 5가지 필드 (0) 2026.06.27 LangGraph 조건부 라우팅 — 상태 값으로 다음 노드를 결정하는 방법 (0) 2026.06.27