Claude, GPT-4 같은 구독형 클라우드 API는 토큰 비용이 누적될 수 있습니다.
Agentic AI 워크플로우는 단일 실행 당 수천 ~ 수만 토큰을 소비하기 때문에 운영 비용이 빠르게 증가합니다.
로컬 GPU 머신에 오픈소스 모델을 직접 올리면 추론 비용 = 전기세 + 하드웨어 감가만 남습니다.
| 항목 | 값 |
|---|---|
| GPU | NVIDIA TITAN V × 4 (VRAM 12 GB × 4 = 48 GB) |
| CUDA | 12.2 / Compute 7.0 (Volta) |
| 서빙 스택 | Ollama v0.24.0 |
| 모델 | Qwen3 30B A3B Q4_K_M (qwen3:30b-a3b-q4_K_M, ~18 GB) ← 현재 사용 중 |
| GPU 분산 | OLLAMA_SCHED_SPREAD=true — GPU 0-3 균등 분산 |
| 컨텍스트 길이 | 8,192 토큰 (OLLAMA_CONTEXT_LENGTH=8192 — KV cache GPU fit 필수) |
| Flight API | SerpAPI (실제 Google Flights 데이터) |
TITAN V (Volta CC 7.0) 주의: bfloat16 / Flash Attention 2 하드웨어 미지원.
Ollama는 float16으로 자동 폴백하므로 정상 동작합니다.
Q4 = 4-bit 양자화. VRAM이 부족하면 Q3/Q2도 가능하나 품질이 저하됩니다.
MoE = Mixture of Experts. 전체 파라미터 중 일부(active)만 추론에 사용 → VRAM 효율 높음.
tool-call 열은bind_tools/with_structured_output호환 여부입니다.
| 모델 | 구조 | 최소 VRAM | tool-call | 특징 |
|---|---|---|---|---|
| Qwen3-235B-A22B | MoE (22B active) | 132 GB (Q4) | ✅ | Alibaba, 프론티어급 품질, Apache 2.0 |
| Kimi K2.6 | MoE (32B active) | 80 GB (Q2) | ✅ | Moonshot AI, 에이전트 스웜(300 sub-agents), 코딩 최강 |
| Llama-4-Maverick | MoE | 80 GB+ | ✅ | Meta, 멀티모달, 대형 에이전트 워크플로우 |
| Gemma 4 26B A4B Q8_0 | MoE (4B active) | 28 GB (Q8) | ✅ | 초기 검증 모델 — 속도 느림, Qwen3로 교체 |
| 모델 | 구조 | 최소 VRAM | tool-call | 특징 |
|---|---|---|---|---|
| Qwen3-30B-A3B Q4_K_M ✅ | MoE (3B active) | 18 GB (Q4) | ✅ | 현재 사용 중 — TITAN V 4장 검증. 플래너 ~63s |
| Qwen3-32B | Dense | 19 GB (Q4) | ✅ | Qwen3 최대 Dense, 범용 최우수 가성비 |
| Qwen3.6-35B-A3B | MoE (3.5B active) | 12 GB (Q4) | ✅ | VRAM 대비 품질 효율 탁월 |
| Nemotron-Cascade-2-30B | Dense | 22 GB (Q4) | ✅ | NVIDIA, ~54 tok/s (소비자 GPU), GPT-4o mini급 |
| Devstral-Small-2-24B | Dense | 18 GB (Q4) | ✅ | Mistral, 에이전트 코딩 특화, SWE-bench 상위 |
| Mistral-Large-3 | Dense | 32 GB (Q4) | ✅ | Mistral 최신, 함수 호출 안정성 우수 |
| Gemma-4-27B | Dense | 20 GB (Q4) | ✅ | Google 최신, 멀티모달, 긴 컨텍스트 |
| 모델 | 구조 | 최소 VRAM | tool-call | 특징 |
|---|---|---|---|---|
| Qwen3-8B Q4_K_M ✅ | Dense | 5.2 GB (Q4) | ✅ | 검증 완료 — 단일 GPU fit, 플래너 ~35s, 빠른 테스트용 |
| Llama-4-Scout-17B | MoE | 10 GB (Q4) | ✅ | Meta MoE, 2026년 5월 기준 경량 전체 1위 |
| Qwen3-14B | Dense | 10 GB (Q4) | ✅ | 소형 중 tool-call 안정성 최상 |
| Gemma-3-12B-Instruct | Dense | 9 GB (Q4) | ✅ | 경량 대비 품질 우수 |
| Phi-4-reasoning-plus | Dense | 12 GB (Q4) | ✅ | Microsoft, reasoning 특화 |
| Qwen3-7B | Dense | 6 GB (Q4) | ✅ | 초경량, CPU fallback 실용적 |
| Mistral-Small-3.2-22B | Dense | 16 GB (Q4) | ✅ | 빠른 추론, 경량 최강 함수 호출 |
| GPU 환경 | 권장 모델 | Ollama 모델명 | 플래너 속도 |
|---|---|---|---|
| TITAN V × 4 (48 GB) | Qwen3-30B-A3B Q4_K_M ✅ 검증 완료 | qwen3:30b-a3b-q4_K_M |
~63s |
| TITAN V × 4 (빠른 테스트) | Qwen3-8B Q4_K_M ✅ 검증 완료 | qwen3:8b |
~35s |
| 48 GB 이상 (RTX 3090 × 2 등) | Qwen3-32B | qwen3:32b |
— |
| 24 GB (RTX 4090 1장) | Qwen3-32B (Q4, 19GB) | qwen3:32b |
— |
| 20~24 GB | Nemotron-Cascade-2-30B 또는 Devstral-Small-2-24B | nemotron-cascade2:30b |
— |
| 16 GB | Mistral-Small-3.2 또는 Qwen3.6-35B-A3B | qwen3.6:35b-a3b |
— |
| 8~12 GB | Llama-4-Scout-17B 또는 Qwen3-14B | llama4:scout |
— |
Gemma 4 26B A4B Q8_0 (
gemma4:26b-a4b-it-q8_0, 28 GB): 초기 검증 모델. PCIe overhead + Q8_0 양자화 조합으로 4-GPU 분산 시 속도가 느림 (~수 분/호출). Qwen3 계열로 교체됨.
v0.24.0부터 배포 포맷이
.tgz→.tar.zst로 변경되었습니다.
설치 스크립트(scripts/setup_ollama_gemma.sh)를 사용하거나 아래 절차를 따르세요.
# 의존성
sudo apt-get install -y zstd
# GitHub Releases에서 직접 다운로드 (공식 install.sh는 환경에 따라 실패할 수 있음)
wget https://github.com/ollama/ollama/releases/download/v0.24.0/ollama-linux-amd64.tar.zst \
-O /tmp/ollama.tar.zst
zstd -d /tmp/ollama.tar.zst -o /tmp/ollama.tar
# 전체 tar 압축 해제 (CUDA 라이브러리 포함 — 이 단계가 GPU 가속의 핵심)
mkdir -p ~/.local
tar -xf /tmp/ollama.tar -C ~/.local/
# PATH 추가
export PATH="$HOME/.local/bin:$PATH"
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
# 모델 다운로드
ollama pull qwen3:30b-a3b-q4_K_M
# Ollama 실행 — CONTEXT_LENGTH(GPU fit) + SCHED_SPREAD(4-GPU 균등 분산) 필수
OLLAMA_CONTEXT_LENGTH=8192 OLLAMA_SCHED_SPREAD=true OLLAMA_NUM_PARALLEL=1 ollama serve &CUDA 라이브러리 누락 주의:
bin/ollama만 추출하면 GPU가 감지되어도
offloaded 0/31 layers to GPU상태로 CPU 모드로 폴백됩니다.
반드시 전체 tar를~/.local/에 추출해야lib/ollama/cuda_v12/libggml-cuda.so가 배포됩니다.
OLLAMA_CONTEXT_LENGTH 설정이 필요한 이유:
| 컨텍스트 길이 | KV Cache 크기 | 모델+Cache 합계 | GPU 결과 |
|---|---|---|---|
| 262,144 (기본) | 매우 큼 | 48 GB 초과 | ❌ CPU 폴백 |
| 8,192 (Q8_0) | 작음 | ~31 GB | ✅ GPU 100% (Q8_0 기준) |
| 16,384 (Q4_K_M) | 작음 | ~21 GB | ✅ GPU 100% (권장) |
# GPU 로드 확인
ollama ps
# NAME ID SIZE PROCESSOR CONTEXT
# qwen3:30b-a3b-q4_K_M a1b2c3d4e5f6 21 GB 100% GPU 8192GPU 메모리 자동 언로드 (keep_alive):
Ollama는 마지막 요청 후 일정 시간이 지나면 모델을 GPU에서 자동으로 내립니다.
| 상황 | 언로드 시점 |
|---|---|
| 기본값 | 마지막 요청 후 5분 뒤 자동 언로드 |
keep_alive 지정 |
해당 시간 후 언로드 |
keep_alive: -1 |
프로세스 종료 전까지 영구 유지 |
keep_alive: 0 |
요청 완료 즉시 언로드 |
에이전트 실행 중에는 요청이 계속 들어오므로 모델이 유지되고, 마지막 요청(ReflectionAgent) 완료 후 5분이 지나면 자동으로 내려갑니다. 다음 실행 시 재로드에 약 30초가 소요됩니다.
# 모델 사전 로드 + 유지 시간 지정 (에이전트 실행 전 권장)
curl -s http://localhost:11434/api/generate \
-d '{"model":"qwen3:30b-a3b-q4_K_M","prompt":"hi","stream":false,"keep_alive":"10m"}' > /dev/null
# 항상 올려두려면 Ollama 서버 실행 시 환경 변수 추가
OLLAMA_KEEP_ALIVE=-1 OLLAMA_CONTEXT_LENGTH=8192 OLLAMA_SCHED_SPREAD=true ollama serve &pip install vllm
python -m vllm.entrypoints.openai.api_server \
--model google/gemma-4-27b-it \
--quantization awq \
--tensor-parallel-size 4 \
--max-model-len 8192 \
--port 8000- OpenAI 호환 엔드포인트:
http://localhost:8000/v1 - 배치 처리·연속 배칭 지원 → 동시 요청에 강함
git clone https://github.com/ggerganov/llama.cpp && cd llama.cpp
cmake -B build -DLLAMA_CUDA=ON && cmake --build build -j
./build/bin/llama-server \
-m ./models/gemma-4-26b-a4b-it-q8_0.gguf \
--port 8080 \
--n-gpu-layers 31- OpenAI 호환 엔드포인트:
http://localhost:8080/v1
# 로컬 LLM 전환
LLM_PROVIDER=local
LOCAL_LLM_BASE_URL=http://localhost:11434/v1 # Ollama
LOCAL_LLM_MODEL=qwen3:30b-a3b-q4_K_M # 또는 qwen3:8b (빠른 테스트)
LOCAL_LLM_API_KEY=ollama
# Ollama 서버 실행 시 필수 환경 변수
OLLAMA_CONTEXT_LENGTH=8192 # GPU fit — 기본값(262144)에서는 CPU 폴백 발생
OLLAMA_SCHED_SPREAD=true # 멀티 GPU 균등 분산 (없으면 GPU 3대에만 편중)
# 클라우드 LLM으로 복구 시 (agents/ 코드 변경 불필요 — LLM_PROVIDER만 전환)
# LLM_PROVIDER=anthropic
# ANTHROPIC_API_KEY=sk-ant-...
# LLM_MODEL=claude-haiku-4-5-20251001이미 LLM_PROVIDER 기반 스위칭이 구현되어 있습니다:
LLM_PROVIDER: str = os.getenv("LLM_PROVIDER", "anthropic")
LOCAL_LLM_BASE_URL: str = os.getenv("LOCAL_LLM_BASE_URL", "http://localhost:11434/v1")
LOCAL_LLM_MODEL: str = os.getenv("LOCAL_LLM_MODEL", "qwen2.5:72b-instruct-q4_K_M")
LOCAL_LLM_API_KEY: str = os.getenv("LOCAL_LLM_API_KEY", "ollama")Gemma 4는 thinking 모드가 활성화되면 {"thought": "..."} 형태의 JSON을 먼저 출력합니다.
with_structured_output 이 이를 Pydantic 스키마로 파싱하려 하면 필드 불일치로 ValidationError가 발생합니다.
해결 방법: 에이전트 목적에 따라 thinking 전략을 분리합니다.
| 에이전트 | 팩토리 | thinking | 이유 |
|---|---|---|---|
| SearchAgent | make_llm |
False | tool call 정밀도 우선 |
| PriceAnalysisAgent | make_thinking_llm |
2단계 | 히스토리·전략 메모·RAG 종합 판단 |
| NotificationAgent | Python 직접 실행 | 없음 (LLM 미사용) | 이메일 발송·메모리 기록은 판단 불필요 — LLM 제거로 6분+ 대기 문제 해결 |
| ReflectionAgent | make_thinking_llm |
2단계 | 트렌드 분석·전략 수립 |
| PlannerAgent | make_thinking_llm |
2단계 | 로컬 LLM(Gemma) None 반환 방지 — 추론 후 안정적 structured output |
| ProactivityAgent | make_thinking_llm |
2단계 | 복합 컨텍스트 판단 |
class TwoStepLLM:
"""local 전용: think=True 자유 텍스트 추론 → think=False structured_output 포맷팅."""
def invoke(self, messages):
reasoning = self._reasoning_llm.invoke(messages) # 1단계: 자유 추론
format_messages = list(messages) + [
AIMessage(content=reasoning.content or ""),
HumanMessage(content="Based on your analysis above, fill in the structured output fields exactly."),
]
return self._format_llm.invoke(format_messages) # 2단계: 구조화 포맷
def make_llm(tools=None, structured_output=None, thinking=False):
"""tool call / structured_output 에이전트용 — thinking=False 고정."""
if config.LLM_PROVIDER == "local":
llm = ChatOpenAI(..., model_kwargs={"extra_body": {"options": {"think": thinking}}})
else:
llm = ChatAnthropic(model=config.LLM_MODEL, temperature=0)
...
def make_thinking_llm(structured_output):
"""추론이 필요한 에이전트용 — provider-agnostic 2단계 호출.
local : think=True 추론(1단계) → think=False structured_output 포맷(2단계)
claude : 내장 추론으로 직접 structured_output (코드 변경 불필요)
"""
if config.LLM_PROVIDER == "local":
return TwoStepLLM(
reasoning_llm=_make_local_llm(thinking=True),
format_llm=_make_local_llm(thinking=False).with_structured_output(structured_output),
)
else:
return ChatAnthropic(model=config.LLM_MODEL, temperature=0).with_structured_output(structured_output)
extra_body사용 이유:model_kwargs={"options": ...}는 OpenAI Python 클라이언트가 알 수 없는 파라미터로 거부합니다.extra_body를 쓰면 클라이언트가 검증 없이 JSON body에 병합하여 Ollama로 전달합니다.
cloud 전환 시 코드 변경 불필요:
LLM_PROVIDER=anthropic으로 바꾸면make_thinking_llm이 Claude 직접 호출로 자동 전환됩니다. Claude는 내부 추론이 내장되어 있어 2단계 분리가 필요 없습니다.
각 파일에서의 import:
# agents/planner.py — 2단계 thinking (로컬 LLM None 반환 방지, 안정적 structured output)
from core.llm import invoke_with_retry, make_thinking_llm
llm = make_thinking_llm(SearchPlan)
# agents/proactivity.py — thinking 유지 (복합 컨텍스트 판단)
from core.llm import make_thinking_llm
llm = make_thinking_llm(ProactivityAnalysis)# Ollama 서버 시작 (CONTEXT_LENGTH + SCHED_SPREAD 필수)
OLLAMA_CONTEXT_LENGTH=8192 OLLAMA_SCHED_SPREAD=true OLLAMA_NUM_PARALLEL=1 ollama serve &
# (선택) 모델 사전 로드 — 첫 에이전트 호출 지연 방지
curl -s http://localhost:11434/api/generate \
-d '{"model":"qwen3:30b-a3b-q4_K_M","prompt":"hi","stream":false,"keep_alive":"10m"}' > /dev/null
# Goal Decomposition 모드 — 자연어 목표를 자동으로 검색 계획으로 분해 (SerpAPI)
python -m examples.flight_monitor.run \
--mode serpapi \
--goal "ICN에서 동남아 30만원 이하 최저가를 2026년 7월~2027년 1월 사이에서 찾아줘" \
--max-price 300000 --currency KRW --user <이름>
# 기본 단일 노선 모니터링 모드 (mock)
python -m examples.flight_monitor.run \
--origin ICN --dest NRT --date 2026-07-15 --max-price 250000LangChain의 bind_tools / with_structured_output은 모델이 tool call 형식을 지원해야 동작합니다.
| 기능 | Ollama | vLLM | 권장 모델 |
|---|---|---|---|
bind_tools |
instruct-tuned 모델 필수 | 대부분 지원 | qwen3:30b-a3b-q4_K_M ✓ |
with_structured_output |
지원 (JSON mode) | 지원 | qwen3:30b-a3b-q4_K_M ✓ |
| 멀티 GPU 레이어 분산 | 자동 | --tensor-parallel-size 설정 |
Ollama 자동 ✓ |
Qwen3 계열 및 Gemma 4
-it태그 모델 모두 tool call을 지원합니다.
non-instruct 모델(-it태그 없음)은 tool call을 지원하지 않습니다.
| 구분 | 비용 |
|---|---|
| Claude Sonnet (API) | 입력 $3 / 출력 $15 per 1M 토큰 |
| 로컬 TITAN V × 4 (전기세) | 약 $0.04 |
| 로컬 RTX 4090 (전기세) | 약 $0.05~0.10 / 시간 (전력 450W 기준) |
하루 수백만 토큰을 처리하는 워크플로우라면 수주 내 하드웨어 비용 회수가 가능합니다.
- GPU 드라이버 및 CUDA 설치 확인 (
nvidia-smi) — TITAN V × 4 (CC 7.0) 확인 - Ollama v0.24.0 전체 tar 추출 (CUDA 라이브러리 포함)
-
qwen3:30b-a3b-q4_K_M모델 다운로드 (~18 GB) -
OLLAMA_CONTEXT_LENGTH=8192 OLLAMA_SCHED_SPREAD=true로 Ollama 실행 → GPU 4개 균등 분산 확인 -
.env에LLM_PROVIDER=local,LOCAL_LLM_MODEL=qwen3:30b-a3b-q4_K_M설정 -
make_llm/make_thinking_llm/TwoStepLLM팩토리 구현 — 에이전트별 thinking 전략 분리 -
agents/planner.py—make_thinking_llm(SearchPlan)(로컬 LLM None 반환 방지) -
notification_node— Python 직접 실행으로 전환, LLM 제거 (6분+ 타임아웃 문제 해결) -
core/llm.py— 로컬 LLMtimeout=300설정 -
agents/proactivity.py—make_thinking_llm유지 (복합 컨텍스트 판단) -
model_kwargs options→extra_body전환 (OpenAI 클라이언트 호환) - Goal Decomposition: 목표가 이하 옵션 최대 20개 수집 → 통합 이메일 발송
- SerpAPI 실제 항공 데이터로 Goal Decomposition 모드 end-to-end 검증
- 결과 이메일 발송 확인 — 옵션 카드 반복 포맷 (━ 구분선)
검증 결과 (2026-05-22 ~ 2026-05-24):
[2026-05-22] 모델: Gemma 4 26B A4B Q8_0
탐색 노선: ICN → FUK/KIX/NRT/PVG (일본·중국 4개 도시)
예산: 300,000 KRW
탐색 결과 (가격순):
1. ICN→FUK 2026-06-09 102,600 KRW T'Way Air TW 207 ← 최저가 ✅
2. ICN→FUK 2026-06-30 102,600 KRW T'Way Air TW 205 ✅
3. ICN→KIX 2026-06-16 117,600 KRW T'Way Air ✅
4. ICN→NRT 2026-07-07 138,300 KRW ZIPAIR Tokyo ZG 46 ✅
5. ICN→PVG 2026-06-23 286,000 KRW Shandong SC 4620 ✅
이메일: 🎉 목표가 이하 항공권 5개 발견! 최저 102,600 KRW → (수신자 이메일)
(5개 옵션 카드 + 노선별 예약 딥링크 포함)
PlannerAgent: ~2-3분 (Gemma 4, 4-GPU PCIe overhead)
[2026-05-23] 모델: Gemma 4 26B A4B Q8_0
탐색 노선: ICN → TAO (칭다오) 왕복, 오전 10시~오후 6시 출발
예산: 400,000 KRW / --ticket-type round-trip (귀환일 PlannerAgent 자동 생성)
탐색 결과: LLM이 자율 결정 (상한 없음), 12개 목표가 이하
최저가: ICN→TAO 2026-11-18 → 2026-11-25 327,500 KRW Shandong SC 4610
이메일: 🎉 목표가 이하 항공권 12개 발견! 최저 327,500 KRW (즉시 발송, 4초)
NotificationAgent: LLM 제거 → Python 직접 실행 (이전 6분+ 타임아웃 해결)
[2026-05-24] 모델: Qwen3 8B Q4_K_M (속도 비교용)
탐색 노선: ICN → HRB (하얼빈) 왕복
예산: 400,000 KRW / waypoints 경유지 파싱 검증
PlannerAgent (5 타겟): ~35초 ← Gemma 4 대비 4~6배 빠름
경로 표시: ICN → TAO → HRB (Shandong 청도 경유 자동 파싱)
목표가 이하 없음 (최저가 474,400 KRW, 2026-07-05)
[2026-05-24] 모델: Qwen3 30B A3B Q4_K_M ← 현재 사용 중
탐색 노선: ICN → HRB (하얼빈) 왕복
예산: 400,000 KRW
PlannerAgent (5 타겟): ~63초
재계획 포함 전체: ~4분 30초
최저가: 474,400 KRW (2026-07-05, ICN → TAO → HRB, Shandong SC 4612)