Zettelkasten

qmd MCP는 한국어 작은 vault에서 Claude grep 베이스라인을 능가하지 못한다

·수정 1

요약

  • 한국어 Obsidian vault(1106 docs) + Claude Sonnet 환경의 30-쿼리 파일럿에서, Claude + grep/Read이 Claude + qmd MCP보다 모든 면에서 동률 이상: 정확도 96.7% vs 93.3%, 속도 2.1× 빠름, 비용 14% 저렴.
  • 그러나 retrieval-only 벤치(qmd bench)에서는 하이브리드(BM25+벡터)가 BM25-only를 압도 (recall 79% vs 32%). 즉 "qmd 자체는 좋은 검색 엔진"이지만, Claude가 똑똑하면 grep만으로 같은 결과에 도달한다.
  • 결론: 도구의 가치는 사용자 능력의 함수다. 약한 검색 클라이언트(LLM이 전략을 못 짜는 경우)에는 qmd가 의미 있고, 강한 검색 클라이언트(Sonnet+)에는 grep 베이스라인이 이미 충분하다.

본문

실험 설계

  • vault: ~/Documents/Obsidian 1106 markdown (Zettelkasten + 회사 프로젝트 + 자료 노트, 한국어 위주, 영어 기술 용어 혼합)
  • 인덱싱: qmd v2.1.0, embeddinggemma-300M-Q8_0 (default), 1448 chunks / 14.2 MB DB / 40s 임베딩 시간
  • eval set: 30 쿼리, 카테고리 5개(exact / semantic / topical / cross-domain / alias) + unanswerable 4개 + 랜덤 샘플 5개
  • 두 조건:
    • A (grep): Claude --allowed-tools "Bash,Glob,Grep,Read" — rg/find/Read만 허용
    • B (qmd): Claude --allowed-tools "mcp__qmd__*,Read" + qmd MCP stdio
  • 동일 prompt, --max-budget-usd 0.50 캡, --model sonnet, 60 세션 ($4.09 총비용)

Retrieval-only 결과 (qmd bench)

backend P@k Recall MRR F1 Avg latency
bm25 0.333 0.322 0.333 0.327 1ms
vector 0.589 0.675 0.600 0.598 25ms
hybrid ★ 0.633 0.786 0.596 0.633 3288ms
full (w/ rerank) 0.589 0.797 0.595 0.597 7982ms

핵심:

  • BM25 단독은 한국어에서 약함 — SQLite FTS5가 한국어 형태소 분석기 없음.
  • Vector(embeddinggemma)는 60% F1로 BM25를 압도. multilingual 약점이 있어도 한국어 의미 검색에 충분.
  • Hybrid(BM25+vector+RRF)가 sweet spot.
  • Rerank는 비용 대비 효과 없음 — full pipeline이 hybrid보다 점수도 낮고 2.4× 느림. cross-encoder가 한국어에서 잘못 판단하는 케이스가 보임.

Agent-in-loop 결과

메트릭 grep qmd Δ
hit rate 96.7% 93.3% grep +3.4%p
avg turns 2.87 3.70 grep -0.83
avg duration 10.8s 23.0s grep 2.1× 빠름
avg cost $0.063 $0.073 grep -14%
avg input tokens 57,297 81,516 grep -30%

McNemar paired exact p = 1.0 — 정확도 차이는 통계적 noise (n=30 너무 작음, codex 리뷰가 미리 경고).

paired 분포:

  • both hit: 27 / grep only: 2 / qmd only: 1 / neither: 0

왜 Claude+grep이 이기는가

  1. Sonnet은 충분히 똑똑하다 — 한국어 패러프레이즈를 자체적으로 keyword set으로 변환해 grep. 동의어 재시도까지 자율적으로 함.
  2. Obsidian zettel 파일명이 descriptive — "비잔틴 장군 문제는 악의적인 노드가 포함됐을 때를 가정한 분산 합의 문제.md" 같은 한 문장 제목 → 키워드 grep만으로 충분.
  3. qmd MCP는 콜드 스타트 부담 — stdio transport는 매 세션마다 query-expansion 1.7B + reranker 0.6B 모델 로드. 단일 호출이 빨라야 하는 시나리오에 비효율.
  4. qmd는 더 큰 컨텍스트 반환 — snippets + scores 포함 → Claude가 후속 Read를 더 함 → 토큰 +30%.

qmd가 의미 있는 1 케이스 (n=30 중)

exact-kafka 쿼리(Kafka 구조):

  • grep: 더 길고 구체적인 zettel을 답으로 골라 정답 kafka.md 놓침
  • qmd: BM25가 짧은 정확 매칭 kafka.md를 1순위로 정확히 골라냄

→ 짧은 영문 식별자 키워드 + 정확한 매치를 원할 때 qmd BM25가 가끔 유리.

qmd v2.1.0의 제약 (별도 발견)

dist/store.js가 search-time query embedding 모델로 embeddinggemma를 하드코딩. QMD_EMBED_MODEL 환경변수는 qmd embed에만 적용됨. 인덱스를 Qwen3/bge-m3 등으로 빌드하면 dimension mismatch로 SIGABRT. → qmd의 한국어 성능 천장이 embeddinggemma-300M의 multilingual 능력에 묶임. bge-m3 같은 한국어 강자 모델 swap이 무의미.

일반화 한계 (codex 리뷰 반영)

  • n=30은 파일럿 — 통계적 결론에는 100200개 필요. 카테고리 813개당으로 확대해야 차이 신뢰구간 수렴.
  • 카테고리 분포는 의도 표본 — 실제 사용 분포가 아님. 실제 검색 실패 로그 + 랜덤 + unanswerable 비중 조정 필요.
  • 벤치/에이전트 prompt가 confounder를 다 통제하지 못함 — Claude의 grep 전략이 자율적이라 베이스라인 강도가 모델 능력에 의존.
  • HTTP daemon mode 미측정 — stdio는 콜드 스타트 매번. HTTP daemon이면 warm 후 1~2초 단축 예상되나, 그래도 grep <100ms와 비교 못함.
  • vault가 1106 docs로 작음 — qmd가 빛을 발하는 영역은 grep으로 후보 좁히기 어려운 큰 코퍼스(>10k docs).

함의

도구는 사용자 능력의 함수다. 평가는 도구의 절대 성능이 아니라 "이 사용자가 이 도구로 얻는 추가 가치"로 측정해야 한다.

  • 자기 vault < 5000 docs + Claude Sonnet 이상 → qmd 도입 효용 의문
  • 자기 vault > 10,000 docs 또는 약한 grep 클라이언트(작은 모델, 비대화형) → qmd 가치 있을 가능성
  • 측정 없이 "RAG가 좋다"고 도구 도입 X. 본인 코퍼스로 grep + 지능형 클라이언트 베이스라인 먼저 측정.

참고