LLM Wiki 기반 개발 에이전트 지식 전달 파이프라인: 로컬 RAG MCP 구축기
개발을 이어 가다 보면 예전에 남긴 아키텍처 결정, 조사 메모, 기술 레퍼런스가 필요해진다. 문제는 그 자료가 현재 프로젝트 안에 있지 않다는 점이다. Obsidian PARA Vault에 잘 정리해 두었더라도, 에이전트가 필요한 내용을 정확히 찾아 근거와 함께 가져오게 만드는 일은 별개의 문제다.
이번에는 Vault의 Markdown을 원본으로 유지하면서, Claude Code와 Codex가 필요한 순간에만 관련 문단을 검색하도록 하는 로컬 RAG MCP를 만들었다. 목표는 "Vault 전체를 AI에게 열어 두기"가 아니라, 제한된 범위의 지식을 출처와 함께 안전하게 꺼내 쓰기였다.
이 글은 구현 방법을 나열하기보다, 어떤 제약에서 출발했고 어떤 설계 결정을 내렸는지 정리한 구축 회고다.
PARA Vault와 LLM Wiki의 역할
PARA Vault는 프로젝트, 지속적으로 관리하는 영역, 재사용할 자원, 보관 자료를 나누어 개인 지식을 관리하는 Markdown 기반 저장소다. 이 볼트에서는 10_Projects/, 20_Areas/, 30_Resources/, 40_Archives/가 PARA의 운영 공간을 구성하며, 99_Raw/는 수집한 원본을 별도로 보관한다. Git은 변경 이력을 남기고, Markdown은 어떤 도구에서도 읽을 수 있는 원본 형식으로 유지한다.
LLM Wiki는 이 Vault 안에서 원본을 바로 답변에 쓰지 않고, 검토 가능한 지식으로 정리하는 운영 방식이다. 흐름은 Ingest → Compile → Auto-tag → Reflect/Clean-MD로 이어진다. Ingest는 99_Raw/와 Inbox에서 후보를 찾고, Compile은 후보를 30_Resources/의 지식 노트로 정리한다. Auto-tag과 Reflect는 컴파일된 지식 계층을 대상으로 메타데이터와 구조를 점검한다.
99_Raw/는 변경하지 않는 원본 계층이다. 반면 30_Resources/는 원문을 바탕으로 컴파일한 지식 계층이다. 개념 노트와 도구·서비스 같은 엔터티 노트가 이곳에 쌓이며, 위키링크와 인덱스로 서로 연결된다. 이번 RAG MCP가 기본적으로 검색하는 범위도 30_Resources/다.
운영은 preview-first를 원칙으로 한다. Ingest, Compile, 메타데이터 점검, Reflect는 먼저 제안과 보고서를 만들고, 실제 노트를 바꾸는 작업은 검토한 뒤 명시적으로 --apply를 실행할 때만 수행한다. 제안은 00_Meta/.memory/ 아래에 남긴다. 컴파일한 노트에는 출처를 담는 sources와 검토 상태를 담는 review_status를 보존한다. 이 메타데이터는 지식의 provenance와 검토 이력을 함께 남긴다.
PARA Vault 원본이 LLM Wiki를 거쳐 30_Resources/ 지식 계층이 되고, 읽기 전용 RAG MCP를 통해 개발 에이전트에 전달되는 흐름이다.
먼저 정한 원칙: 검색은 하되, 자동으로 주입하지 않는다
개인 Vault에는 프로젝트 자료만 있지 않다. 장기 계획, 개인 메모, 아직 검증하지 않은 자료도 섞일 수 있다. 그래서 처음부터 다음 원칙을 정했다.
- Vault의 Markdown은 계속 원본(source of truth)으로 둔다.
- 에이전트가 모든 대화에 Vault를 자동 주입하지 않는다.
- 필요한 순간에만 검색하고, 결과에는 경로와 헤딩을 함께 돌려준다.
- 검색 서버는 읽기 전용으로 동작한다.
- 색인과 검색은 로컬에서 처리하며, 외부 HTTP 포트를 열지 않는다.
자동 컨텍스트 주입은 편해 보이지만, 관련 없는 개인 정보가 프롬프트에 섞일 수 있고 토큰도 꾸준히 사용한다. 반대로 명시적으로 검색하는 방식은 한 번의 도구 호출이 더 필요하지만, 무엇을 왜 읽었는지 추적하기 쉽다.
이 서비스의 역할도 좁혔다. 검색 결과는 답의 근거 후보일 뿐이다. 에이전트는 결과의 원문을 다시 열어 확인해야 하며, 오래되었거나 초안 상태인 노트는 그 상태를 답변에 반영해야 한다.
MCP 도구는 세 개만 남겼다
MCP(Model Context Protocol) 서버가 제공하는 도구를 최소화했다.
| 도구 | 역할 |
|---|---|
retrieve_context | 질의와 선택 스코프로 관련 청크를 검색한다. |
read_source | 검색 결과의 원문과 특정 헤딩을 다시 읽어 검증한다. |
index_status | 현재 색인 세대, 색인 시각, 대상 수, 오류 수를 확인한다. |
쓰기를 위한 도구와 refresh_index 같은 도구는 제공하지 않는다. 검색 서버가 Vault를 수정할 수 없게 하면 원문 무결성의 경계가 훨씬 분명해진다. 색인도 서버에 맡기지 않고 별도 CLI로 분리했다.
검색 결과에는 단순한 발췌문 외에도 Vault 상대 경로, 제목, 헤딩, 점수, 검색 근거를 담는다. 예를 들어 semantic-match, keyword-match, tag-match 같은 이유를 반환한다. 이 정보가 있어야 에이전트와 사용자가 "왜 이 문단이 나왔는지" 판단할 수 있다.
재색인 중에도 검색을 멈추지 않는 방법: generation 교체
가장 중요한 설계는 색인과 서빙을 분리한 것이다. 처음에는 실행 중인 MCP 서버가 색인까지 처리하도록 만들 수도 있어 보였다. 하지만 stdio MCP는 다른 프로세스가 호출할 수 있는 일반적인 IPC 채널을 제공하지 않는다. 색인 작업을 서버에 위임하는 구조는 생각보다 자연스럽지 않았다.
그래서 두 프로세스로 나눴다.
PARA Vault Markdown
│
▼
para-vault-rag index ──▶ 새 generation 생성·검증
│
원자적으로 current 교체
│
▼
para-vault-rag serve ──▶ Claude Code / Codex
para-vault-rag index는 변경된 노트를 찾아 새 generation 디렉터리에 DuckDB 인덱스, 검색용 토큰 코퍼스, 매니페스트를 모두 만든다. 생성이 끝난 뒤 스키마와 행 수, 매니페스트를 검증한다. 모든 검증이 성공했을 때만 current 포인터를 원자적으로 바꾼다.
이 방식의 장점은 단순하다. 색인이 실패하면 기존 current는 그대로 남는다. 검색 서버는 언제나 완성된 이전 세대 또는 완성된 새 세대만 읽는다. 반쯤 작성된 인덱스를 읽을 일이 없다.
serve 프로세스는 요청마다 current가 바뀌었는지만 가볍게 확인한다. 세대가 바뀐 경우에만 기존 읽기 전용 연결을 닫고 새 세대를 다시 연다. 따라서 재색인 중에도 검색을 중단하거나 서버를 재시작할 필요가 없다.
한국어와 기술 용어를 함께 찾기 위해 하이브리드 검색을 사용했다
개인 Vault의 질의는 두 유형이 섞인다. DuckDB, 특정 라이브러리 이름처럼 정확한 단어를 찾는 질의가 있고, "기존 개발 아키텍처 결정"처럼 과거 판단을 자연어로 찾는 질의가 있다. 한 가지 검색 방식만으로는 둘 다 만족하기 어렵다.
그래서 두 검색 결과를 결합했다.
- 의미 검색: 로컬에서 실행하는 다국어 임베딩 모델
BAAI/bge-m3로 의미가 비슷한 문단을 찾는다. - 키워드 검색:
kiwipiepy로 한국어를 토큰화한 뒤 Okapi BM25로 정확한 용어와 형태 변화를 찾는다. - 순위 결합: 두 점수를 그대로 더하지 않고 Reciprocal Rank Fusion(RRF)으로 순위만 결합한다.
점수의 단위가 다른 BM25와 코사인 유사도를 임의의 가중치로 더하면 튜닝이 불안정해지기 쉽다. RRF는 각 검색기의 순위를 결합하므로, 정확한 기술명 검색과 의미상 가까운 과거 메모를 함께 상위 후보에 올리기에 적합했다.
특히 한국어에서는 단순 공백 분리가 충분하지 않다. 파일, 파일을, 파일이처럼 조사가 붙은 질의가 달라지기 때문이다. 테스트에는 조사형과 원형이 유사한 BM25 결과를 반환하는지 확인하는 사례를 넣었다.
권한은 검색 품질보다 먼저 설계했다
RAG에서 검색 범위는 편의 기능이 아니라 권한 경계다. 이번 MVP는 Vault 전체를 색인하지 않는다. 30_Resources/만 resources라는 별칭으로 허용했다.
DEFAULT_SCOPE = "resources"
SCOPES = {
"resources": "30_Resources/",
}
여기서 중요한 점은 scope가 임의의 경로나 glob을 받지 않는다는 것이다. 허용 목록에 없는 별칭, 99_Raw 같은 경로, 상위 디렉터리로 빠져나가려는 요청은 검색을 시작하기 전에 거부한다. read_source도 같은 경계를 적용한다.
새 범위를 추가하려면 로컬 설정 파일 한 줄을 바꾸는 방식이 아니라, 코드 변경과 테스트, 보안 검토를 거쳐야 한다. 이 불편함은 의도적이다. 검색 범위를 넓히는 일은 기능 옵션이 아니라 데이터 노출 범위를 넓히는 결정이기 때문이다.
기존 파일시스템 MCP를 제거한 이유
처음에는 RAG MCP와 기존 파일시스템 MCP를 함께 둘 생각이었다. 검색 품질은 RAG가 담당하고, 일반 파일 접근은 기존 도구가 담당하는 구조다.
하지만 새 Claude 세션에서 경계를 검증하는 과정에서 문제가 재현됐다. 등록할 때 지정한 30_Resources/가 아니라, 세션의 작업 디렉터리를 기준으로 허용 경로가 바뀌었다. 재등록 후 새 세션에서도 동일한 문제가 나타났다.
원인은 설치된 파일시스템 서버가 클라이언트의 Roots 지원 여부에 따라 인자로 전달한 허용 경로 대신 클라이언트 Roots를 사용한 데 있었다. 이 상황에서 파일시스템 MCP를 계속 유지하면, 필요한 기능보다 불확실한 접근 표면이 더 커진다.
결국 파일시스템 MCP를 제거하고, cross-project Vault 접근을 para-vault-rag로 통합했다. 이 서비스는 검색, 인용, 원문 검증이라는 필요한 기능만 제공한다. 더 많은 권한을 주는 대신 더 복잡한 격리를 덧붙이는 것보다, 처음부터 권한이 작은 도구로 경계를 다시 정의하는 편이 낫다고 판단했다.
검증은 기능보다 경계를 중심으로 했다
테스트는 "검색 결과가 나온다"에서 멈추지 않았다. 다음 조건을 자동화해 확인했다.
- 등록된 MCP 도구가 정확히 세 개인지
- 서버가 DuckDB 읽기 전용 연결만 여는지
- 등록되지 않은 스코프와 임의 경로를 거부하는지
- 한국어 조사형 질의가 같은 주제의 문서를 찾는지
- 색인 중에도 이전 generation으로 검색이 계속되는지
- 색인이 끝난 뒤 서버 재시작 없이 새 generation을 읽는지
- 검색 결과가 Vault 상대 경로와 헤딩을 포함하는지
운영 검증에서도 허용된 resources 결과는 경로와 헤딩으로 다시 읽을 수 있었고, 등록되지 않은 raw 스코프와 범위 밖 메타 파일 읽기는 각각 구조화된 오류로 거부됐다.
성능 수치는 현재 개발 장비에서 200개 대표 노트를 대상으로 측정했다. 전체 색인의 3회 중앙값을 기준으로 한 SLO는 25.638초, 노트 한 개를 수정한 뒤 증분 색인의 SLO는 16.359초였다. 최초 모델 로드를 포함한 콜드 실행은 82.707초로 더 길었으므로, 반복 운영의 SLO와 모델 부트스트랩 지연은 분리해 기록했다.
실제 운영 generation에서는 100개 노트가 1,041개 청크로 색인됐고, 인덱스 오류는 0건이었다. 숫자 자체보다 중요한 것은, 이 결과가 원본 Markdown을 변경하지 않고 재생성 가능한 캐시에만 쌓인다는 점이다.
다음 단계: 더 많이 열기보다 더 잘 평가하기
MVP를 마친 뒤 바로 더 많은 폴더를 열 계획은 없다. 다음 과제는 실제 Vault를 기준으로 검색 품질을 평가하는 일이다.
먼저 대표 질의와 기대하는 출처 노트를 버전 관리되는 평가 세트로 만든다. 정확한 기술명, 한국어 자연어 질의, 과거 결정 탐색 질의를 함께 넣고 현재 순위를 기준선으로 기록한다. 그 뒤에야 후보 수, RRF 상수, 의미 검색과 키워드 검색의 균형, 문서 다양성 같은 신호를 조정할 수 있다.
품질 평가: 검색 결과로 확인한 현재 상태
검색 인덱스는 정상이다. 현재 generation은 20260726T064440.997570Z-b874b363이며, 100개 노트와 1,041개 청크를 보관하고 색인 오류는 0건이다.
기존 개발 아키텍처 결정 질의에서 확인한 기존 결정은 다음과 같다.
- Markdown과 Git을 유지하며 별도 DB는 도입하지 않는다.
99_Raw/는 immutable 원본 계층으로,30_Resources/는 컴파일된 지식 계층으로 관리한다.- 기본 동작은 preview이며, 실제 변경은 명시적인
--apply로만 허용한다. - provenance와
review_status를 메타데이터로 보존한다. - React는 타입별 폴더보다 기능 단위 구조를 우선한다.
- API 호출은 service layer로 이동하고 서버 상태는 React Query로 관리한다.
- 전면 rewrite가 아닌 feature 단위의 점진적 마이그레이션을 수행한다.
R1 조정 결과는 같은 generation에서 측정했다. 실제 Vault 15개 평가 사례의 hit rate는 0.786에서 1.000으로, pass rate는 0.333에서 0.867로 올랐다. 기존 개발 아키텍처 결정 질의에서는 WebSocket/SSE, Expo 구조, Claude Code 구조 관련 노트가 검색됐다. 더 구체적인 기존 React 개발 아키텍처 질의에서는 React 아키텍처 노트가 1순위로 반환됐다.
검색 결과는 관련 노트를 보여 주는 출발점이다. 반환된 경로와 헤딩을 확인하고, 필요하면 read_source로 원문을 다시 읽어야 한다. 검색 품질이 개선됐더라도, 검색 결과 자체가 결정을 자동으로 확정하지는 않는다.
정리
RAG는 기억을 대체하는 시스템이 아니다. 사람이 남긴 기록을 더 빠르게 찾고, 다시 검증하도록 돕는 검색 계층이다. 이 원칙을 지킨다면 개인 Vault는 에이전트가 사용할 수 있는 유용한 근거 저장소가 되면서도, 여전히 개인이 통제하는 원본으로 남을 수 있다.
이번 구축에서 얻은 결론은 다음과 같다.
- 개인 Vault를 연결할 때는 검색 성능보다 접근 경계를 먼저 설계해야 한다.
- 색인과 서빙을 generation 교체로 분리하면 재색인 중에도 안정적으로 읽기 서비스를 유지할 수 있다.
- 검색 결과에 출처와 검증 경로를 포함해야 RAG가 그럴듯한 답변 생성기가 아니라 신뢰할 수 있는 조사 도구가 된다.
- 실제 Vault 평가 세트에서 hit rate 1.000, pass rate 0.867을 기록했지만, 검색 결과는 원문 검증을 대신하지 않는다.
Vault를 개발 에이전트에 연결하려는 경우라면, 처음에는 작은 스코프와 읽기 전용 도구로 시작하는 것을 권한다. 필요한 범위는 나중에 늘릴 수 있지만, 한 번 넓어진 권한 경계를 다시 줄이는 일은 언제나 더 어렵다.