로컬 LLM 에이전트 구축 가이드 — Ollama·LangGraph·MCP(Model Context Protocol)로 코드 리뷰 에이전트와 RAG 파이프라인 구현하기
GPT-4o나 Claude API를 프로젝트에 도입할 때 두 가지 장벽이 자주 등장합니다. 첫 번째는 프라이버시입니다. 사내 코드베이스나 기밀 문서를 외부 서버로 전송해도 괜찮은지 법무팀과 협의해야 합니다. 두 번째는 비용입니다. 토큰 기반 과금은 사용량이 늘수록 예측하기 어렵습니다. 로컬 AI 에이전트는 이 두 문제를 동시에 해결합니다. LLM을 자신의 머신에서 직접 실행하면 데이터가 네트워크 밖으로 나가지 않고, API 청구서도 사라집니다.
Ollama의 월간 다운로드가 5천만 회를 돌파하고 LangGraph가 1.0 정식 버전을 출시하면서 로컬 에이전트 구성의 진입 장벽이 실질적으로 낮아졌습니다. Anthropic이 설계한 MCP(Model Context Protocol)가 에이전트-도구 간 통신 표준으로 자리잡으며 생태계도 빠르게 성숙하고 있습니다. 이 글에서는 로컬 AI 에이전트의 3계층 아키텍처를 이해하고, API 키 없이 동작하는 코드 리뷰 에이전트와 완전 로컬 RAG 파이프라인을 직접 만드는 핵심 지식을 얻을 수 있습니다.
이 글에서 만들 것
- 예시 1 —
codellama:7b+ LangGraph로 동작하는 오프라인 코드 리뷰 에이전트- 예시 2 —
mistral:7b+ ChromaDB로 구성하는 완전 로컬 RAG 파이프라인- 예시 3 — MCP(Model Context Protocol) 도구 서버를 LangGraph 에이전트에 연결하는 패턴
사전 조건: Python 기초 문법과 pip 사용 경험이 있으면 따라 할 수 있습니다.
하드웨어: 7B 파라미터 모델 기준 최소 8GB VRAM이 권장됩니다. GPU가 없더라도 CPU로 실행할 수 있지만 속도가 크게 느려집니다. Apple Silicon Mac(M1 이상)은 통합 메모리를 활용해 16GB 환경에서도 쾌적하게 동작합니다.
핵심 개념
로컬 AI 에이전트란 무엇인가
로컬 AI 에이전트는 단순한 챗봇과 다릅니다. 챗봇이 질문 → 대답의 1회성 왕복으로 끝난다면, 에이전트는 Reasoning → Acting → Observing의 ReAct 루프를 반복하며 다단계 작업을 스스로 계획하고 수행합니다.
ReAct(Reasoning + Acting): LLM이 "어떤 도구를 어떤 순서로 써야 할지" 추론(Reasoning)하고, 실제 도구를 호출(Acting)한 뒤, 결과를 관찰(Observing)하는 사이클을 반복하는 에이전트 패턴입니다. 2023년 논문 "ReAct: Synergizing Reasoning and Acting in Language Models"에서 제안되었으며, LangGraph는 이 패턴을 그래프 형태로 구현합니다.
모든 추론이 로컬 머신 안에서 완결된다는 점이 클라우드 에이전트와의 가장 본질적인 차이입니다. 데이터가 네트워크 밖으로 나가지 않습니다.
3계층 아키텍처
로컬 AI 에이전트는 세 계층의 조합으로 구성됩니다.
| 계층 | 역할 | 대표 구현체 |
|---|---|---|
| 추론 엔진 | 로컬 LLM 서빙 | Ollama, LM Studio, vLLM |
| 에이전트 프레임워크 | 도구 호출·메모리·워크플로 | LangGraph, CrewAI, AutoGen |
| 도구/통합 레이어 | 외부 시스템 연결 | MCP, FastAPI, Docker |
각 계층은 어댑터를 통해 연결됩니다. LangGraph에서 Ollama를 사용하려면 langchain-ollama 패키지가 필요하고, MCP 도구 서버를 LangGraph에 연결하려면 langchain-mcp-adapters가 필요합니다. 추론 엔진을 Ollama에서 vLLM으로 교체하더라도 LangGraph 워크플로 코드는 대부분 그대로 유지됩니다.
MCP — 에이전트 도구 통신의 새 표준
2024년 Anthropic이 설계하고 2025년 12월 Linux Foundation에 기증된 MCP(Model Context Protocol)는 에이전트가 외부 도구(파일 시스템, 데이터베이스, 브라우저 등)와 통신하는 방식을 표준화합니다. OpenAI와 Google도 공식 지원을 선언했으며, Claude 환경에서는 이미 75개 이상의 MCP 커넥터가 제공됩니다.
MCP(Model Context Protocol): 에이전트가 "어떤 도구가 있고, 어떻게 호출하는지"를 동적으로 탐색하고 실행할 수 있도록 정의한 표준 프로토콜입니다. USB-C가 다양한 기기를 통일된 방식으로 연결하듯, MCP는 다양한 LLM과 외부 도구를 일관된 인터페이스로 연결합니다.
추론 엔진 선택 가이드
| 도구 | 특징 | 적합 대상 |
|---|---|---|
| Ollama | CLI 우선, OpenAI 호환 REST API, 완전 오픈소스 | 개발자, API 통합 |
| LM Studio | GUI 기반, Apple Silicon MLX 최적화 | 비개발자, Mac 사용자 |
| Jan | 오픈소스, 프라이버시 우선 ChatGPT 대안 | 개인 사용자 |
| vLLM | 고성능 배치 서빙, 프로덕션 배포 최적화 | 엔터프라이즈 |
개발자라면 Ollama를 먼저 시작해보시는 것을 권장합니다. OpenAI SDK와 호환되는 REST API를 기본 제공하므로 기존 코드베이스에 통합하기가 수월합니다.
실전 적용
예시 1: Ollama + LangGraph로 오프라인 코드 리뷰 에이전트 구성
API 키 없이 codellama:7b 모델을 로컬에서 실행하고, LangGraph로 린트 → 리뷰 순서의 워크플로를 구성합니다. codellama를 선택한 이유는 코드 이해와 생성에 특화 훈련된 모델이기 때문입니다. 동일한 7B 크기라도 범용 모델인 mistral보다 코드 리뷰 맥락에서 더 관련성 높은 피드백을 제공합니다.
# 1. Ollama 설치 후 모델 다운로드
brew install ollama # macOS
# Linux: curl -fsSL https://ollama.com/install.sh | sh
ollama pull codellama:7b # 약 3.8GB, 코드 특화 모델
# 2. 의존성 설치 (버전 명시)
pip install "langgraph>=0.2" "langchain-ollama>=0.1" "langchain-core>=0.3"# code_review_agent.py
from langchain_ollama import ChatOllama
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator
import subprocess
# 상태 정의
# TypedDict: 노드 간에 전달되는 데이터의 구조와 타입을 선언합니다.
# LangGraph는 이 구조를 기반으로 상태를 직렬화하고 노드 사이에서 안전하게 전달합니다.
# Annotated[list[str], operator.add]: 여러 노드가 issues에 값을 추가할 때
# 기존 리스트를 덮어쓰지 않고 자동으로 합치는 리듀서(reducer)를 지정합니다.
class ReviewState(TypedDict):
file_path: str
code: str
lint_result: str
review_result: str
issues: Annotated[list[str], operator.add]
iteration: int
# 로컬 LLM 초기화 (API 키 불필요)
llm = ChatOllama(
model="codellama:7b",
base_url="http://localhost:11434", # Ollama 기본 엔드포인트
temperature=0.1, # 낮을수록 일관된 출력
)
# 린트 노드 — ruff로 정적 분석 후 결과를 상태에 저장
def lint_node(state: ReviewState) -> ReviewState:
result = subprocess.run(
["ruff", "check", state["file_path"]],
capture_output=True, text=True
)
return {**state, "lint_result": result.stdout or "린트 오류 없음"}
# 리뷰 노드 — 코드와 린트 결과를 LLM에 전달하여 리뷰 생성
def review_node(state: ReviewState) -> ReviewState:
prompt = f"""다음 코드를 리뷰하고 개선이 필요한 점을 JSON 형식으로 반환하세요.
코드:
{state['code']}
린트 결과:
{state['lint_result']}
응답 형식: {{"issues": ["문제1", "문제2"]}}"""
response = llm.invoke(prompt)
return {
**state,
"review_result": response.content,
"iteration": state["iteration"] + 1,
}
# 그래프 구성: lint → review → END
graph = StateGraph(ReviewState)
graph.add_node("lint", lint_node)
graph.add_node("review", review_node)
graph.set_entry_point("lint")
graph.add_edge("lint", "review")
graph.add_edge("review", END)
agent = graph.compile()
# 실행 — with 블록으로 파일 핸들이 반드시 닫히도록 보장
with open("my_module.py", "r") as f:
code = f.read()
result = agent.invoke({
"file_path": "my_module.py",
"code": code,
"lint_result": "",
"review_result": "",
"issues": [],
"iteration": 0,
})
print(result["review_result"])# 실행
python code_review_agent.py| 코드 포인트 | 설명 |
|---|---|
ChatOllama(base_url=...) |
로컬 Ollama 서버에 연결. API 키 파라미터 없음 |
TypedDict + Annotated |
노드 간 상태의 타입을 선언하고, 리스트 필드에 리듀서를 지정해 병렬 노드 실행 시 안전한 병합을 보장 |
lint_node → review_node |
린터 실행과 LLM 리뷰를 별도 노드로 분리해 각 단계를 독립적으로 테스트 가능하게 구성 |
with open(...) |
컨텍스트 매니저로 파일 핸들이 반드시 닫히도록 보장 |
예시 2: 사내 문서 RAG 파이프라인 (완전 로컬)
민감한 사내 문서를 외부로 전송하지 않고 검색 증강 생성(RAG)을 구성하는 패턴입니다. 코드 리뷰 에이전트와 달리 mistral:7b를 사용하는 이유는, Mistral이 범용 텍스트 이해와 요약에 더 적합하게 훈련되어 있기 때문입니다. 코드 특화 작업에는 codellama가, 자연어 문서 기반 QA에는 mistral이 더 나은 결과를 제공합니다.
# Ollama 모델 다운로드
ollama pull mistral:7b # 약 4.1GB, 범용 텍스트 처리
ollama pull nomic-embed-text # 274MB, 로컬 임베딩 전용
# 의존성 설치 — langchain-ollama는 OllamaEmbeddings 포함
pip install "langchain-ollama>=0.1" "langchain-chroma>=0.1" chromadb \
"langchain-community>=0.3" "langchain-core>=0.3"# local_rag_pipeline.py
# LangChain 0.3 이상 기준으로 작성
from operator import itemgetter
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_chroma import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import DirectoryLoader
from langchain_core.prompts import ChatPromptTemplate
# 1. 로컬 임베딩 모델 (외부 API 호출 없음)
embeddings = OllamaEmbeddings(model="nomic-embed-text")
# 2. 문서 로드 및 청크 분할
loader = DirectoryLoader("./company_docs", glob="**/*.md")
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
)
chunks = splitter.split_documents(docs)
# 3. 로컬 벡터 DB에 저장
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db", # 로컬 디스크에 저장
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
# 4. RAG 체인 구성 (LangChain 0.3+ 권장 LCEL 문법)
# itemgetter로 딕셔너리 키를 안전하게 추출
llm = ChatOllama(model="mistral:7b", temperature=0)
prompt = ChatPromptTemplate.from_template("""
다음 컨텍스트만을 사용하여 질문에 답하세요.
컨텍스트에 없는 내용은 "문서에서 찾을 수 없습니다"라고 답하세요.
컨텍스트:
{context}
질문: {question}
""")
rag_chain = (
{
"context": itemgetter("question") | retriever,
"question": itemgetter("question"),
}
| prompt
| llm
)
# 5. 질의 — rag_chain 끝에 ChatOllama가 있으므로 반환값은 AIMessage 객체
response = rag_chain.invoke({"question": "온보딩 절차가 어떻게 되나요?"})
print(response.content)# 실행
python local_rag_pipeline.py예시 3: MCP 도구 서버를 LangGraph 에이전트에 연결하기
MCP(Model Context Protocol)의 핵심 가치는 도구 서버와 에이전트의 분리입니다. 도구 서버를 독립적으로 배포하고, 에이전트는 MCP 클라이언트를 통해 사용 가능한 도구를 동적으로 탐색해 호출합니다. 이 예시는 MCP 도구 서버 구성 → 클라이언트 연결 → 에이전트 실행의 전체 흐름을 보여줍니다.
Docker Compose 기반 프로덕션 배포와 Playwright 브라우저 자동화 연동은 별도 글에서 심화 다룰 예정입니다.
# MCP 어댑터 및 서버 의존성 설치
pip install "langchain-mcp-adapters>=0.1" "langchain-ollama>=0.1" \
fastapi uvicorn1단계: FastAPI MCP 도구 서버 구성
# tool_server/main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class BrowseRequest(BaseModel):
url: str
@app.post("/tools/extract_text")
async def extract_text(req: BrowseRequest) -> dict:
"""지정된 URL의 텍스트를 추출합니다."""
# 실제 구현에서는 httpx 또는 playwright 사용
return {"result": f"{req.url}에서 추출한 텍스트 (예시)"}
# MCP 도구 목록 엔드포인트 — 프로토콜 요구사항
@app.get("/mcp/tools")
async def list_tools():
return {
"tools": [
{
"name": "extract_text",
"description": "URL에서 텍스트를 추출합니다",
"inputSchema": {
"type": "object",
"properties": {"url": {"type": "string"}},
"required": ["url"],
},
}
]
}2단계: MCP 클라이언트로 에이전트에 도구 등록
# mcp_agent.py
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
from langchain_ollama import ChatOllama
llm = ChatOllama(model="mistral:7b", temperature=0.1)
async def main():
# MCP 클라이언트가 도구 서버에서 사용 가능한 도구 목록을 자동으로 탐색
async with MultiServerMCPClient(
{
"browser-tools": {
"url": "http://localhost:8000/mcp",
"transport": "streamable_http",
}
}
) as mcp_client:
# 탐색된 도구를 LangChain 호환 형태로 변환 후 에이전트에 등록
tools = mcp_client.get_tools()
agent = create_react_agent(llm, tools)
response = await agent.ainvoke(
{"messages": [{"role": "user", "content": "https://example.com 페이지의 텍스트를 추출해줘"}]}
)
print(response["messages"][-1].content)
asyncio.run(main())# 터미널 1: 도구 서버 실행
uvicorn tool_server.main:app --port 8000
# 터미널 2: 에이전트 실행
python mcp_agent.py| 코드 포인트 | 설명 |
|---|---|
MultiServerMCPClient |
여러 MCP 도구 서버를 동시에 연결하는 클라이언트 |
mcp_client.get_tools() |
서버에서 사용 가능한 도구 목록을 동적으로 탐색해 LangChain 호환 도구로 변환 |
create_react_agent |
LangGraph의 ReAct 에이전트를 간단히 생성하는 헬퍼. LLM과 도구 목록만 전달하면 됨 |
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 완전한 데이터 프라이버시 | 데이터가 로컬을 벗어나지 않아 HIPAA·GDPR 등 규제 준수 가능 |
| API 비용 없음 | 토큰 과금 없이 무제한 실행 가능 |
| 낮은 지연시간 | 네트워크 왕복 없이 로컬 추론으로 응답 속도 개선 |
| 완전한 커스터마이징 | 모델 파인튜닝, 시스템 프롬프트, 파라미터 자유 조정 |
| 오프라인 동작 | 인터넷 연결 없이도 작동 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 하드웨어 요구사항 | 7B 모델 최소 8GB VRAM, 70B 모델 48GB 이상 필요 | CPU 추론(느리지만 가능), 양자화 모델(Q4_K_M, Q8_0) 활용 |
| 모델 성능 격차 | GPT-4o·Claude Opus 대비 추론 품질이 낮을 수 있음 | 태스크에 맞는 특화 모델 선택 (코드: codellama, 범용: mistral) |
| 설정 복잡도 | 환경 구성·모델 관리에 기술적 지식 필요 | Docker Compose로 환경 표준화, LM Studio GUI 활용 |
| 프롬프트 인젝션 위험 | 에이전트가 외부 데이터와 지시문을 구분 못하는 보안 취약점 | 입력 데이터 샌드박싱, 화이트리스트 기반 도구 접근 제한 |
| 모델 업데이트 관리 | 수동으로 최신 모델 다운로드·교체 필요 | ollama pull 주기적 실행 자동화 |
프롬프트 인젝션(Prompt Injection): 에이전트가 처리하는 외부 데이터(웹 페이지, 파일 등)에 악의적인 지시문이 포함되어 에이전트 행동을 의도치 않게 조작하는 공격입니다. 에이전트가 파일 시스템이나 웹에 접근할수록 이 위험이 커집니다.
자주 하는 실수 TOP 3
- VRAM 확인 없이 큰 모델 선택: GPU 메모리를 확인하지 않고 13B 이상 모델을 선택해 CPU 추론으로 전락하는 경우입니다.
ollama ps로 현재 로드 상태를 먼저 확인하고, 하드웨어에 맞는 양자화 버전(Q4_K_M, Q8_0)을 선택하는 것이 좋습니다. 8GB VRAM이라면 7B Q8 또는 13B Q4 수준이 현실적인 선택지입니다. - 에이전트에 과도한 권한 부여: 테스트 편의를 위해 에이전트에 전체 파일 시스템·네트워크 접근을 허용하는 경우입니다. 최소 권한 원칙(Principle of Least Privilege)을 적용해 에이전트가 실제로 필요한 디렉터리·URL만 접근하도록 제한하는 것을 권장합니다.
- LLM 응답 형식을 신뢰: LLM이 항상 지정한 JSON 형식으로 응답한다고 가정하는 경우입니다. 로컬 모델은 클라우드 API 대비 형식 준수율이 낮을 수 있으므로, Pydantic 파싱과 재시도 로직을 포함하는 것이 좋습니다.
response_format파라미터를 지원하는 모델이라면 이를 활용하는 방법도 있습니다.
마치며
이제 API 키 없이 코드 리뷰 에이전트를 실행할 수 있고, 사내 문서를 외부에 전송하지 않고도 시맨틱 검색 시스템을 운영할 수 있습니다. Ollama·LangGraph·MCP(Model Context Protocol)의 조합은 오늘 당장 실무에 투입 가능한 수준에 도달했습니다.
지금 바로 시작해볼 수 있는 3단계:
- Ollama를 설치하고 기존 OpenAI SDK 코드의
base_url만 바꿔 로컬 전환을 테스트해보세요.brew install ollama && ollama run mistral:7b한 줄로 터미널에서 바로 대화를 시작할 수 있습니다.http://localhost:11434/v1/chat/completions에 OpenAI 호환 API가 즉시 활성화되므로, 기존 코드의base_url한 줄만 변경해도 로컬 전환을 바로 확인할 수 있습니다. - 예시 1의 코드 리뷰 에이전트를 본인 GitHub 저장소의 파일 하나에 적용해보세요.
pip install "langgraph>=0.2" "langchain-ollama>=0.1"로 의존성을 설치한 뒤file_path변수만 변경하면 됩니다. 처음에는 단일 노드 그래프로 시작하고, 점차 도구와 노드를 추가해 나가는 방식으로 확장해볼 수 있습니다. - 사내 문서 10개를 대상으로 RAG 파이프라인을 먼저 구성해보세요. 소규모로 시작해 검색 결과의 관련도를 확인한 뒤, 문서 수와 청크 크기를 점진적으로 조정하는 방식으로 품질을 개선할 수 있습니다.
nomic-embed-text+ ChromaDB 조합은 274MB 임베딩 모델 하나로 수만 개의 문서를 오프라인에서 검색할 수 있는 수준입니다.
다음 글: 멀티에이전트 협업 패턴 심화 — CrewAI와 LangGraph로 역할 기반 팀을 구성하고 복잡한 연구·코딩 파이프라인을 자동화하는 방법
참고 자료
- Building Local AI Agents with LangGraph and Ollama | DigitalOcean
- Building a Local AI Agent with Ollama + MCP + LangChain + Docker | DEV Community
- Running Local LLMs in 2026: Ollama, LM Studio, and Jan Compared | DEV Community
- Top 9 AI Agent Frameworks as of March 2026 | Shakudo
- Agentic AI and Security | Martin Fowler
- Best practices for AI agent security in 2025 | Glean
- Introducing Strands Agents, an Open Source AI Agents SDK | AWS 기술 블로그