사후 분석: Ollama와 ChromaDB를 사용하여 코드베이스 메모리를 위한 로컬 MCP 서버 구축하기
요약
개인정보 보호와 비용 문제를 해결하기 위해 Ollama와 ChromaDB를 활용하여 로컬 MCP 서버를 구축하는 방법을 다룹니다. zerikai_memory 프로젝트를 통해 로컬 모델이 코드베이스 메모리를 어떻게 처리하는지 분석합니다.
핵심 포인트
- Ollama를 활용해 데이터 유출 없이 로컬에서 AI 에이전트 실행 가능
- ChromaDB를 통한 검색 계층과 로컬 모델의 합성 과정 분석
- 8GB VRAM 환경에서의 하드웨어 제약 사항 및 성능 고려
- mistral:7b 모델 사용 시 Ollama 로우 모드를 통한 함수 호출 구현
개발자들은 클라우드 API 과금과 독점적인 코드베이스를 제3자 엔드포인트로 전송하는 것에 따른 개인정보 보호 위험에 대해 반발하고 있습니다. 올해의 한 Hacker News 스레드는 이를 명확하게 설명했습니다. 문제는 토큰당 가격이 아니라, AI 에이전트가 지속적으로 API를 폴링(polling)할 때 발생하는 사용량 기반 과금의 예측 불가능성입니다. Reddit에서는 개인정보 보호에 대한 우려가 더 극명하게 나타납니다. 기업 및 국방 업무의 경우, 비용과 관계없이 회사의 지적 재산(IP)을 OpenAI나 Anthropic으로 보내는 것은 절대 불가합니다.
zerikai_memory는 바로 이러한 목적을 위해 local 모드를 제공합니다. 모든 것은 Ollama를 통해 실행되며, 아무것도 사용자의 기기를 떠나지 않습니다. 우리는 mistral:7b를 기본 로컬 모델로 출시했습니다. 2026년 6월에는 에이전트 기반 코딩(agentic coding) 작업을 위해 특별히 훈련된 ornith:9b가 출시되어, 두 모델을 모두 테스트했습니다. 다음은 우리가 발견한 내용입니다.
zerikai_memory가 로컬 모델을 사용하는 방식
zerikai_memory는 세 가지 모드로 작동합니다: cloud (DeepSeek), local (Ollama), 그리고 hybrid입니다. 이들 사이의 라우팅(routing)은 main.py:986의 _should_use_cloud()에 의해 처리되며, 명시적 오버라이드(explicit override), 키워드 일치, 단어 수 임계값, 그리고 MEMORY_MODE 환경 변수(env var) 폴백(fallback) 순서의 4단계 우선순위 체인을 따릅니다. 로컬 모드에서 해당 함수는 항상 false를 반환하며, 모든 것은 기기 내에서 유지됩니다.
로컬 모드에서 모든 합성(synthesis) 호출은 main.py:1555의 _query_ollama를 호출합니다. 모델은 _load_project_context로부터 프로젝트 브리프(brief)를 받고, 여기에 함수 시그니처(function signatures), 파일 경로, 라인 범위, 독스트링(docstrings)과 같은 구조화된 ChromaDB 엔티티 페이로드(entity payloads)를 더해 전달받습니다. 모델은 인라인 #file:line 인용이 포함된 답변을 반환합니다. 단일 호출이며, 스트리밍(streaming)이나 도구 루프(tool loop)는 없습니다.
검색 계층(retrieval layer) (ChromaDB + L2 거리 + 어휘적 재순위화(lexical rerank))은 모델에 구애받지 않습니다. 두 모델 모두 모든 쿼리 테스트에서 동일한 컨텍스트(context)를 받았습니다. 유일한 변수는 합성(synthesis)이었습니다.
하드웨어
- GPU: NVIDIA RTX 3050, 8GB GDDR6 전용 VRAM
- CPU: Intel i7-12700
- RAM: 32GB
- OS: Windows 11
8GB 전용 VRAM이 물리적인 한계치(hard ceiling)입니다. Windows는 오버플로(overflow)를 위해 공유 시스템 메모리(shared system memory)를 제공하지만, PCIe를 통한 공유 RAM에서의 추론(inference)은 실무에서 체감될 정도로 느립니다.
모델 (Models)
mistral:7b (v0.3) -- 4.4GB, 32K 컨텍스트(context). Ollama의 로우 모드(raw mode)를 통해서만 함수 호출(Function calling)이 가능합니다. 즉, [AVAILABLE_TOOLS] 프롬프트를 직접 구성하고 [TOOL_CALLS] 응답을 직접 파싱해야 합니다. 2024년 5월 마지막 업데이트되었습니다.
ornith:9b-dense -- Gemma 4 및 Qwen 3.5를 기반으로 구축되었으며, 약 5.5GB로 추정됩니다. 네이티브 도구 호출(tool calling) 기능을 갖추고 에이전트형 코딩(agentic coding) 작업을 위해 강화학습(RL)으로 훈련되었습니다. SWE-Bench Verified에서 69.4, Terminal-Bench 2.1에서 43.1을 기록하며, 파라미터 수가 3배 더 많은 모델들과 대등하거나 이를 능가하는 성능을 보여줍니다.
테스트 1: 벤치마크 스크립트 (지연 시간, Latency)
독립적인 Python 스크립트를 사용하여 실제 zerikai_memory 워크스페이스 엔티티(entities)로 구성된 정적 ChromaDB 페이로드(payload) 샘플을 Ollama에 직접 요청했습니다. 모델당 3개의 쿼리(queries)와 3개의 샘플을 사용했으며, HTTP 계층에서 로우 지연 시간(raw latency)을 측정했습니다. 이 테스트는 실제 코드베이스에 대한 합성 품질(synthesis quality)이 아니라, 하드웨어에서의 추론 속도를 측정합니다.
| 모델 | 평균 (Mean) | 표준편차 (StdDev) | 최소 (Min) | 최대 (Max) |
|---|---|---|---|---|
| mistral:7b | 6.14s | 3.58s | 2.92s | 14.57s |
| ornith:9b | 13.39s | 5.76s | 8.77s | 25.67s |
ornith의 첫 번째 쿼리 시 콜드 스타트(cold start)는 25.67초를 기록했습니다. Ollama가 모델을 고정(pin)하기 전에 공유 메모리로 넘쳐흘렀기(spilled) 때문입니다. 워밍업된(Warmed) 샘플은 917초 사이에 안정되었습니다. mistral은 8GB 내에 깔끔하게 들어가며, 워밍업 후 37초 내에 실행됩니다.
벤치마크 스크립트는 리포지토리(blog/ 디렉토리)에 포함되어 있습니다. 모델 교체를 결정하기 전에 본인의 하드웨어에서 직접 실행해 보십시오.
테스트 2: 라이브 MCP 쿼리 (합성 품질, Synthesis Quality)
두 모델 모두 실제 zerikai_memory 워크스페이스 인덱스(index)를 대상으로 zerikai_memory의 universal-brain MCP 레이어를 통해 라이브 테스트를 진행했습니다. 동일한 5개의 쿼리를 사용했으며, 실행 사이에 모델을 수동으로 교체하고 각 교체 시마다 Ollama를 재시작했습니다. 이 테스트는 합성된 페이로드가 아닌 실제 ChromaDB 검색(retrieval)을 통해 개발자가 Cursor나 VS Code에서 실제로 경험하게 될 내용을 측정합니다.
동일한 쿼리, 동일한 ChromaDB 컨텍스트, 동일한 시스템 프롬프트(system prompt). 결과는 다음과 같습니다.
쿼리 1: 쿼리 라우팅(query routing)은 Ollama와 DeepSeek 사이에서 어떻게 결정하나요?
ornith:9b:
검색된 코드베이스 메모리(codebase memory)를 바탕으로, 쿼리 라우팅(query routing)이 작동하는 방식은 다음과 같습니다:
라우팅 로직 (Routing Logic):
...
mistral:7b:
zerikai_memory의 쿼리 라우팅(query routing)은 4단계 우선순위 체인(priority chain)을 기반으로 Ollama를 사용할지 또는 DeepSeek를 사용할지 결정합니다. 우선순위는 다음과 같습니다:
...
쿼리 2: .brain 디렉토리 경로는 어디에 정의되어 있으며, 운영 체제 전반에서 어떻게 해결(resolve)되나요?
ornith:9b:
`.brain` 디렉토리 경로는 두 가지 핵심 위치를 통해 정의되고 해결(resolve)됩니다:
정의: `config.py`의 1-2행에 베이스(base)를 정의하는 상수가 있습니다...
mistral:7b:
모든 워크스페이스 데이터의 저장 루트(storage root)를 정의하는 데 사용되는 `DB_PATH` 상수는 `config.py`에 정의되어 있습니다:
...
쿼리 3: 백그라운드 브리프 합성(background brief synthesis)은 어떻게 MCP 타임아웃을 방지하나요?
ornith:9b:
백그라운드 브리프 합성(background brief synthesis)은 `asyncio.create_task`로 실행되는 '실행 후 망각(fire-and-forget)' 작업으로 동작함으로써 MCP 타임아웃을 방지합니다. 즉, `_background_brief_synthesis()`를 호출할 때 결과를 기다리거나(awaiting) 반환하지 않습니다. 이를 통해 느린...
mistral:7b:
백그라운드 브리프 합성(background brief synthesis)은 `_background_brief_synthesis` 함수에서 `asyncio.create_task`를 사용하여 비동기 작업 생성(asynchronous task creation)을 활용함으로써 MCP 타임아웃을 방지합니다. 이 접근 방식은 함수가 메인 이벤트 루프를 차단하지 않고 독립적으로 실행될 수 있게 합니다...
쿼리 4: save_to_memory는 tree-sitter와 어떻게 작동하며 어떤 파일 유형을 인덱싱하나요?
ornith:9b:
`save_to_memory`는 파일 확장자에 따라 라우팅합니다. `.py`, `.js`, `.ts`, `.css`, `.html` 또는 `.md` 파일은 tree-sitter 엔티티 추출(entity extraction, `code_indexer.py`의 `extract_entities` 흐름)을 통해 처리됩니다. 다른 확장자는...
mistral:7b:
save_to_memory 함수는 ChromaDB의 영구 벡터 메모리 (persistent vector memory)에 콘텐츠를 저장하는 방식으로 작동합니다. 이 함수는 지원되는 파일 형식인 .py, .js, .ts, .css, .html, .md에 대해 코드 파싱 (code parsing)을 위해 tree-sitter를 사용합니다. 다른 형식은 DeepSeek/Ollama로 대체됩니다...
질문 5: 로컬 모드에서 asyncio 세마포어 (semaphore)가 Ollama 동시성 (concurrency)을 어떻게 제어하나요?
두 모델 모두 이 질문에 완전히 답변할 수 있을 만큼 충분한 인덱싱된 컨텍스트 (indexed context)를 가지고 있지 않았습니다. 각 모델이 그 공백을 어떻게 처리했는지가 이번 테스트에서 가장 중요한 데이터 포인트입니다.
ornith:9b:
사용 가능한 코드베이스 메모리 컨텍스트를 바탕으로, asyncio 세마포어가 로컬 모드에서 Ollama 동시성을 구체적으로 어떻게 제어하는지는 판단할 수 없습니다. 검색된 엔티티 (entities)는 다음과 같습니다:
...
mistral:7b:
이 프로젝트는 로컬 모드에서 Ollama 동시성을 제어하기 위해 asyncio 세마포어를 사용합니다.
asyncio 세마포어가 ...을 관리하는 데 사용된다는 점을 설명함으로써 이 질문에 답변할 수 있습니다.
mistral은 자신 있게 답변했습니다. 인용한 Semaphore(4) 및 Semaphore(2) 값은 실제 값이지만, 이는 쿼리 시점의 Ollama 동시성이 아니라 백그라운드 스캔 파이프라인 (background scan pipeline)에 속하는 것입니다. 모델이 다른 질문에 잘못된 컨텍스트를 적용하고 그 답변을 사실처럼 제시했습니다. 반면 ornith은 정보가 없다고 말하고 멈췄습니다. IDE 어시스턴트에게 있어, 자신감 있는 오답은 정직한 정보의 공백보다 더 큰 비용을 초래합니다.
전제 조건: Docstring 품질
작업 순서가 중요하며, 이는 README에서 명확하게 드러나지 않습니다.
.memignore 파일을 편집한 후, 먼저 코드베이스에 대해 embedding-docstring을 실행하십시오. 그 다음 scan_workspace를 실행하십시오. 만약 docstring이 희소하거나 누락된 상태에서 먼저 스캔을 진행하면, ChromaDB는 얇은 벡터 (thin vectors)를 인덱싱하게 됩니다. 먼저 데이터를 풍부하게 만든(re-enrich) 후 다시 스캔하지 않는 한, 재스캔만으로는 이를 해결할 수 없습니다. 메모리의 품질은 tree-sitter가 추출한 내용에 달려 있으며, tree-sitter는 오직 존재하는 내용만을 추출할 수 있기 때문입니다.
zerikai_memory는 바로 이러한 이유로 embedding-docstring 스킬을 함께 제공합니다. 이 스킬은 Python, JavaScript, TypeScript, HTML을 아우르며, 벡터 임베딩 (vector embedding) 품질을 높이기 위해 전체 워크스페이스의 docstring (독스트링), 주석 블록, 인라인 문서를 검사하고 다시 작성합니다. 누락된 문서는 처음부터 새로 작성하며, 워크스페이스 루트에 있는 .memignore 파일을 준수합니다. 올바른 워크플로우는 다음과 같습니다:
.memignore → embedding-docstring → scan_workspace → query
첫 번째 단계를 건너뛰면 두 모델 모두 성능이 저하됩니다. 실제 문제는 ChromaDB에 입력된 데이터에 있음에도 불구하고, 모델이나 하드웨어를 탓하며 시간을 허비하게 될 것입니다.
현재 상태: pi.dev와 잘 작동하며, 일부 에디터의 대용량 파일 크기 제한 문제로 인해 VS Code 지원을 진행 중입니다. 업데이트: 2026년 7월 14일 기준으로 VS Code가 이제 대용량 파일을 지원하므로, 이 스킬은 Cursor와 VS Code 모두에서 사용할 수 있습니다.
요약 생성 (Brief Generation): 통제되지 않았으나 유용한 데이터 포인트
보조 테스트로서, 우리는 동일한 워크스페이스에 대해 DeepSeek (클라우드, 희소한 docstring)와 ornith:9b (로컬, embedding-docstring 강화 후)가 생성한 요약(briefs)을 비교했습니다. 이것은 통제된 비교는 아닙니다. 실행 간에 docstring 밀도가 달랐기 때문에 모델만이 유일한 변수는 아닙니다.
이 비교가 보여주는 것은, 강화된 ChromaDB 컨텍스트(context)가 주어진 ornith:9b가 원자적 덮어쓰기 의미론 (atomic overwrite semantics), 명명 규칙 분석 (naming convention breakdowns), 문서가 누락된 부분에 대한 명시적 격차 플래그 (explicit gap flags) 등 밀도 높고 정밀한 요약을 생성한다는 점입니다. 반면 희소한 컨텍스트를 대상으로 한 DeepSeek는 코드에 존재하지 않는 일부 추론된 세부 사항을 포함하여 더 얇은 출력을 생성했습니다.
여기서 얻을 수 있는 교훈은 요약 생성에 있어 ornith가 DeepSeek를 이긴다는 것이 아닙니다. embedding-docstring 강화를 통한 효과가 출력물에서 눈에 보이고 측정 가능하다는 점입니다. 컨텍스트가 풍부할 때, ornith는 의미 있는 합성 쿼리 (synthesis queries)를 제공할 수 있을 만큼 충분히 좋은 요약을 생성합니다. 컨텍스트가 풍부하지 않을 때는 어떤 모델도 이를 보완할 수 없습니다.
완전한 로컬 모드 및 요약 합성: 세마포어 수정 (The Semaphore Fix)
이번 릴리스 이전에는 완전한 로컬 모드 (full local mode)에서 GPU 포화 문제가 있었습니다. main.py:538에 위치한 _synthesize_deep_brief는 동시성 제어 장치(concurrency gate) 없이 9개의 요약 섹션 전체에 대해 asyncio.gather를 동시에 실행했습니다. 로컬 모드에서 이는 9개의 Ollama 호출이 한꺼번에 GPU에 전달됨을 의미하며, 8GB 그래픽 카드를 확실히 포화시킬 수밖에 없었습니다.
이 테스트와 함께 수정 사항이 배포되었습니다. 클라이언트 설정 후 main.py에서 초기화되는 전역 ollama_semaphore는 use_cloud=False일 때 _build_section_safe 래퍼(wrapper)를 통해 _build_section 호출을 제어합니다. 클라우드(Cloud) 및 하이브리드(hybrid) 모드는 세마포어를 완전히 우회합니다. DeepSeek는 API 측에서 자체적으로 속도 제한 (rate limiting)을 처리하기 때문입니다.
ollama_semaphore = asyncio.Semaphore(OLLAMA_MAX_CONCURRENCY)
async def _build_section_safe(name):
...
OLLAMA_MAX_CONCURRENCY는 .env를 통해 설정 가능하며, 8GB 하드웨어의 경우 기본값은 1입니다. VRAM 여유 공간이 더 많은 그래픽 카드를 사용하는 사용자는 이 값을 높일 수 있습니다. 이 포스트의 ornith:9b 요약은 이 수정 사항이 적용된 상태에서 생성되었습니다. 이번 릴리스를 기점으로 완전한 로컬 모드의 요약 합성 (brief synthesis) 기능은 프로덕션 환경에서 사용할 준비가 되었습니다.
하드웨어 및 비용
만약 토큰 가격 때문에 이 글을 읽고 계신다면, API 호출에 지출하는 비용 대비 GPU 업그레이드 비용은 다음과 같습니다.
- RTX 3060 12GB (ornith:9b 사용을 위한 권장 최소 사양): 신품 기준 $330-$470. Newegg에서 ASUS Dual 및 Gigabyte WINDFORCE 변형 모델을 약 $340-$440에 구매 가능합니다.
- RTX 4060 Ti 16GB: $400-$500. 추가적인 VRAM 덕분에 시스템 RAM으로 넘기지 않고도 더 큰 13B-14B 양자화 모델 (quantized models)을 로드할 수 있습니다.
- RTX 4070 12GB: 약 $600. 더 빠른 텐서 코어 (Tensor cores)와 빠른 토큰 생성 속도를 제공합니다.
AMD 그래픽 카드 (RX 6700 XT 12GB, 리퍼비시 기준 $380부터)는 동등한 VRAM을 제공하지만 ROCm 설정이 필요합니다. Ollama의 CUDA 경로는 NVIDIA에서 플러그 앤 플레이 (plug-and-play)로 작동합니다. AMD도 작동하지만 설정 오버헤드가 추가됩니다.
8GB (RTX 3050 급)에서는 ornith:9b가 실행은 되지만, 콜드 스타트 (cold starts)가 고통스럽고 VRAM 여유 공간이 매우 타이트합니다. 로컬 zerikai_memory 사용을 위한 실질적인 최적의 선택지(sweet spot)는 RTX 3060 12GB입니다.
권장 사항
ornith:9b는 mistral:7b를 대체하는 새로운 기본 로컬 모델 권장 사항입니다.
8GB 전용 VRAM 환경: ornith가 구동은 가능하지만 여유가 없습니다. Ollama가 모델을 고정(pin)하지 않은 상태에서의 콜드 스타트(Cold start)는 25초가 소요됩니다. 모델을 전환하거나 GPU 워크로드를 동시에 실행하지 않는 로컬 전용 워크플로우라면, 9~17초 사이의 웜 신세시스(Warm synthesis)는 수용 가능한 수준입니다. .env 파일에 OLLAMA_MAX_CONCURRENCY=1을 설정하세요.
10-12GB 전용 VRAM 환경 (RTX 3060 12GB 이상): 모델이 고정된 상태로 유지되어 콜드 스타트 시간이 크게 단축되며, 인용 정밀도(citation precision)가 mistral보다 일관되게 더 뛰어납니다.
8GB 미만의 전용 VRAM을 사용하거나, 인용 정밀도보다 신세시스 지연 시간(synthesis latency)이 더 중요하다면 mistral:7b를 사용하세요. .env 파일에 OLLAMA_MODEL=mistral:7b를 설정하십시오. 이 모델은 컨텍스트(context)가 밀집되어 있을 때 신세시스를 정확하게 처리합니다. 다만 컨텍스트가 희박할 경우, 확신에 차 있지만 틀린 답변으로 공백을 채울 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기