홈으로 돌아가기

RAG와 MCP를 사용한 AI 에이전트: 외부 도구 통합

이 기사는 Model Context Protocol을 통해 외부 API와 상호작용하고 RAG를 통해 내부 문서와 작업할 수 있는 AI 에이전트 조립을 설명합니다. LangGraph, FastMCP 및 ChromaDB를 사용한 Python 구현.

외부 도구용 RAG와 MCP를 사용한 AI 에이전트 조립 방법
Advertisement 728x90

RAG와 외부 도구를 MCP로 활용한 AI 에이전트 구축

현대적인 AI 에이전트는 텍스트 생성을 넘어 외부 API와 상호작용하고, 상태를 관리하며, 맥락에 기반한 결정을 내립니다. 이 글에서는 내부 문서 작업을 위한 RAG와 외부 서비스(날씨와 GitHub Issues)에 접근하기 위한 Model Context Protocol (MCP)을 결합한 에이전트를 구축하겠습니다. 이 아키텍처는 LangGraph와 FastMCP을 기반으로 하며, 커스텀 어댑터를 작성하지 않고도 시스템을 확장할 수 있습니다.

아키텍처 구성 요소: ReAct, RAG 및 MCP

우리의 AI 에이전트는 ReAct 사이클로 작동합니다: 모델이 쿼리를 분석 → 도구 선택 → 결과 획득 → 최종 답변까지 사이클 반복. 이는 단계 순서가 엄격히 미리 정의된 선형 체인과 다릅니다. LangGraph는 의사결정을 지원하기 위해 사용되며, 명시적인 상태 관리와 전환 그래프를 제공합니다.

여기서 RAG는 별도의 모듈이 아니라 에이전트의 도구 중 하나로 작동합니다. 사용자가 내부 문서에 대해 물어보면, 에이전트는 모델의 매개변수 메모리에 의존하는 대신 search_documentation을 호출합니다. 이는 환각(hallucinations) 위험을 줄이고 응답 정확성을 높입니다.

Google AdInline article slot

Model Context Protocol (MCP)은 외부 도구 연결을 표준화합니다. 각 API마다 래퍼를 수동으로 작성하는 대신, JSON-RPC를 통해 도구를 제공하는 MCP 서버를 실행합니다. 에이전트는 클라이언트를 통해 서버에 연결하며, 모든 도구가 자동으로 사용 가능해집니다. 이는 프로덕션 환경에서 통합 수가 빠르게 증가할 때 특히 유용합니다.

기술 스택 및 선택 이유

기술 선택은 확장성, 비동기 지원, MCP 호환성 요구사항에 따라 결정되었습니다:

  • Python 3.11+: 현대적인 async/await 지원, 안정적인 생태계.
  • LangGraph + LangChain: LangGraph는 상태와 ReAct 사이클 제어를 제공하며, LangChain은 모델과 도구 추상화를 담당합니다.
  • FastMCP: 데코레이터를 통해 MCP 서버를 빠르게 생성하는 Python 라이브러리. 2.0 버전은 활발히 개발 중이며 클라이언트 기능도 포함합니다.
  • ChromaDB: 프로토타입에 이상적인 경량 임베디드 벡터 DB.
  • OpenAI text-embedding-3-small: 임베딩에 최적의 가격/성능 비율.
  • Claude 또는 GPT-4: 함수 호출을 지원하는 모든 모델.

외부 도구를 위한 MCP 서버 구현

첫 번째 단계는 날씨 조회와 GitHub Issue 생성이라는 두 도구를 제공하는 MCP 서버 생성입니다. 서버는 FastMCP을 기반으로 하며 stdio 전송을 통해 서브프로세스로 실행됩니다.

Google AdInline article slot
# tools_server.py
import os
import httpx
from fastmcp import FastMCP

mcp = FastMCP("ExternalTools")

@mcp.tool()
async def get_weather(city: str) -> str:
    async with httpx.AsyncClient() as client:
        geo_response = await client.get(
            "https://geocoding-api.open-meteo.com/v1/search",
            params={"name": city, "count": 1, "language": "en", "format": "json"},
            timeout=10.0
        )
        geo_data = geo_response.json()
        if not geo_data.get("results"):
            return f"Gorod '{city}' not found"
        
        location = geo_data["results"][0]
        lat, lon = location["latitude"], location["longitude"]
        
        weather_response = await client.get(
            "https://api.open-meteo.com/v1/forecast",
            params={"latitude": lat, "longitude": lon, "current_weather": True},
            timeout=10.0
        )
        current = weather_response.json()["current_weather"]
        return f"Pogoda in {location['name']}: {current['temperature']}°C, veter {current['windspeed']} km/ch"

@mcp.tool()
async def create_github_issue(repo: str, title: str, body: str, labels: list[str] | None = None) -> str:
    github_token = os.environ.get("GITHUB_TOKEN")
    if not github_token:
        return "Error: GITHUB_TOKEN not ustanovlen"
    
    url = f"https://api.github.com/repos/{repo}/issues"
    headers = {"Authorization": f"Bearer {github_token}", "Accept": "application/vnd.github+json"}
    payload = {"title": title, "body": body}
    if labels: payload["labels"] = labels
    
    async with httpx.AsyncClient() as client:
        response = await client.post(url, json=payload, headers=headers)
        if response.status_code == 201:
            return f"Issue sozdan: {response.json()['html_url']}"
        else:
            return f"Error: {response.status_code}"

if __name__ == "__main__":
    mcp.run(transport="stdio")

구현의 주요 특징:

  • @mcp.tool() 데코레이터는 타입 어노테이션과 docstring을 기반으로 매개변수 JSON 스키마를 자동 생성합니다.
  • 비동기 HTTP 클라이언트를 사용하여 블로킹되지 않는 요청을 처리합니다.
  • 각 도구 로직에 오류 처리가 내장되어 있어 에이전트는 예외 대신 사람이 읽을 수 있는 메시지를 받습니다.
  • Stdio 전송은 클라이언트가 서버를 자식 프로세스로 실행하고 stdin/stdout으로 통신할 수 있게 합니다.

에이전트 도구로서 RAG 모듈 설정

RAG 시스템은 PDF 문서를 인덱싱하고 LangChain 호환 도구를 통해 검색 기능을 제공합니다. 중요한 점은 이 도구가 MCP 도구와 동일한 풀에 등록되어 에이전트가 외부 API와 함께 선택할 수 있다는 것입니다.

# rag_tool.py
import os
from pathlib import Path
from langchain_openai import OpenAIEmbeddings
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain.tools import tool

CHROMA_PATH = "./chroma_db"
DATA_PATH = "./data"

@tool
def search_documentation(query: str) -> str:
    """Ischet relevantnye fragmenty in documentation by request."""
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    vectorstore = Chroma(persist_directory=CHROMA_PATH, embedding_function=embeddings)
    docs = vectorstore.similarity_search(query, k=3)
    return "\n\n".join([f"Withtranitsa {d.metadata.get('page', 'N/A')}: {d.page_content[:500]}..." for d in docs])

# Function initsializatsii database (vyzyvaetsya when starte agenta)
def initialize_vector_store(force_reload=False):
    if os.path.exists(CHROMA_PATH) and not force_reload:
        return
    
    documents = []
    for pdf_file in Path(DATA_PATH).glob("*.pdf"):
        loader = PyPDFLoader(str(pdf_file))
        documents.extend(loader.load())
    
    if not documents:
        return
    
    splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
    chunks = splitter.split_documents(documents)
    
    Chroma.from_documents(chunks, OpenAIEmbeddings(model="text-embedding-3-small"), persist_directory=CHROMA_PATH)

MCP 클라이언트 통합 및 LangGraph에서의 에이전트 조립

마지막 단계는 MCP 도구와 RAG를 결합한 에이전트 조립입니다. langchain-mcp-adapters를 사용하여 MCP 도구를 LangChain 형식으로 변환한 후, RAG 도구와 함께 그래프에 등록합니다.

Google AdInline article slot
# agent.py
from langgraph.graph import StateGraph, END
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, AIMessage
from langchain_mcp_adapters import McpToolNode
from rag_tool import search_documentation
import subprocess
import asyncio

# Run MCP-server how podprotsessa
mcp_process = subprocess.Popen(
    ["python", "tools_server.py"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE
)

# Withbudynek MCP-client and adaptera narzędzieov
mcp_node = McpToolNode(
    server_command=["python", "tools_server.py"],
    transport="stdio"
)

# Receiving list narzędzieov from MCP-server
mcp_tools = asyncio.run(mcp_node.get_tools())
all_tools = [*mcp_tools, search_documentation]

# Initialization modeli and graph
model = ChatAnthropic(model="claude-3-5-sonnet-20240620").bind_tools(all_tools)

# Withstoyanie graph
class AgentState(TypedDict):
    messages: Annotated[list, operator.add]

# Uzly graph
def call_model(state):
    response = model.invoke(state["messages"])
    return {"messages": [response]}

def should_continue(state):
    last_message = state["messages"][-1]
    return "continue" if last_message.tool_calls else "end"

# Postroenie graph
workflow = StateGraph(AgentState)
workflow.add_node("agent", call_model)
workflow.add_node("tools", mcp_node)
workflow.set_entry_point("agent")
workflow.add_conditional_edges("agent", should_continue, {"continue": "tools", "end": END})
workflow.add_edge("tools", "agent")

app = workflow.compile()

# Example run
result = app.invoke({"messages": [HumanMessage(content="Uznay pogodu in Londone and sozday issue in repo/my-project with etim rezultatom")]})
print(result["messages"][-1].content)

중요한 점:

  • MCP는 새로운 도구마다 커스텀 어댑터를 작성할 필요를 없애줍니다. 서버에서 @mcp.tool() 데코레이터만 추가하면 됩니다.
  • RAG는 일반 도구로 통합되어 에이전트가 문서 검색과 외부 API 호출 사이에서 선택할 수 있습니다.
  • LangGraph는 복잡한 다단계 작업에 필수적인 투명한 상태 관리와 ReAct 루프 제어를 제공합니다.
  • 비동기 도구 구현으로 에이전트 메인 스레드를 블로킹하지 않습니다.
  • Stdio 전송은 네트워크 종속성 없이 동일 컨테이너나 머신에서 서버와 에이전트를 실행할 수 있게 해 배포를 단순화합니다.

— Editorial Team

Advertisement 728x90

다음 읽기