Kilka tygodni temu zbudowałem lokalny system RAG. Pliki wrzucone na udział NAS są odbierane przez watchera, osadzane przez Gemini i trafiają do pgvectora. Claude Code przeszukuje je przez narzędzie MCP. Architektura działa. Był jednak jeden kłopotliwy krok: żeby zapisać dokument, najpierw musiałem zapisać go na dysk.

Przepływ wyglądał tak: piszę coś w Claude Code, zapisuję do /mnt/nas/RAG/, czekam aż watcher to odbierze, potwierdzam, że zostało wczytane, i plik znika. Działa, ale irytuje. Plik pojawiał się, służył jako obszar pośredni i znikał. Całość czuła się jak obejście problemu.

Właściwy problem był inny: Obsidian wciąż był w grze. Na NAS miałem vault ze 120 plikami markdown: runbooki, notatki architektoniczne, dokumentacja incydentów, decyzje. Watcher wczytywał je ponownie przy każdej zmianie. Miałem więc dwa systemy robiące to samo: Obsidian zarządzający plikami, które mogłem przeglądać, i RAG zarządzający wektorami, które mogłem przeszukiwać. Rozjeżdżały się. Poprawka zastosowana w jednym miejscu nie zawsze trafiała do drugiego.

Postanowiłem usunąć warstwę plików.

Dodanie rag_save do serwera MCP

Pierwszy krok to dodanie endpointu zapisu do serwera RAG MCP. Istniejący serwer miał tylko rag_search. Dodałem rag_save:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def rag_save(title: str, content: str, tags: str = "", source_type: str = "text", event_date: str = "") -> str:
    """Save a document directly to the RAG knowledge base. Embeds and stores in pgvector without needing a file on disk."""
    slug = title.strip().lower().replace(" ", "-")
    for ch in ",:;!?'\"()[]{}/@#$%^&*~`":
        slug = slug.replace(ch, "")
    source_file = f"{slug}.md"
    ...
    vector = embed_text(content)
    insert_document(content=content, embedding=vector, source_type=source_type,
                    source_file=source_file, metadata=metadata, event_date=event_date.strip() or None)
    return f"Saved: {source_file} ({len(vector)} dims, {len(content)} chars, tags: {tag_list})"

Tytuł staje się identyfikatorem source_file po przekształceniu w slug. To właśnie tego używa upsert do deduplikacji: wywołaj rag_save z tym samym tytułem ponownie, a stary dokument zostanie zastąpiony. Nie trzeba ścieżki do pliku ani watchera.

Funkcja przyjmuje opcjonalny parametr event_date, niezależny od daty zapisu. Przy zapisywaniu dokumentu incydentu lub decyzji sprzed wtorku mogę podać, kiedy zdarzenie faktycznie miało miejsce. Wyniki wyszukiwania pokazują tę datę obok wskaźnika podobieństwa.

Serwer Gradio MCP udostępnia to jako drugi tab obok interfejsu wyszukiwania, a flaga mcp_server=True automatycznie eksponuje funkcję jako narzędzie MCP.

Migracja

Przed usunięciem czegokolwiek sprawdziłem, że każdy plik z Obsidiana jest już w RAG albo daje się poprawnie wyszukać. Potem:

1
rm -rf /mnt/nas/obsidian/

120 plików zniknęło. Baza ma je. System plików nie.

Zmieniły się też wolumeny watchera. Montowanie NAS wskazujące na vault Obsidiana zniknęło z docker-compose.yml. Watcher monitoruje teraz tylko /mnt/nas/RAG/ (tymczasowy obszar drop zone) i katalog z plikami pamięci. Nadal ma dostęp do zapisu w drop zone, żeby móc usuwać pliki po wczytaniu.

1
2
3
4
watcher:
  volumes:
    - /mnt/nas/RAG:/watch/nas          # drop zone, pliki usuwane po wczytaniu
    - /home/tomakl/.claude/projects/.../memory:/watch/memory:ro  # exempt from deletion

Pliki pamięci to jedyny wyjątek. Żyją na dysku i w RAG jednocześnie. Watcher wczytuje je ponownie przy zmianach, ale ich nie usuwa. Wersja plikowa to warstwa strukturalna, wersja wektorowa to warstwa semantyczna.

Jak wygląda praca teraz

Gdy sesja przynosi coś wartego zachowania (decyzja, runbook, notatka z debugowania), zapisuję przez narzędzie MCP:

1
2
3
4
5
6
mcp__rag__rag_save(
  title="LAPA HikariCP connection pool tuning",
  content="...",
  tags="lapa,java,database",
  source_type="runbook"
)

I tyle. Żadnego pliku. Treść jest osadzana i zapisywana. Mogę ją przeszukiwać natychmiast.

Do przeglądania jest web UI pod rag.tomakl.dev (zabezpieczone hasłem, przez tunel Cloudflare). To aplikacja Gradio z tabem wyszukiwania i tabem zapisu. Tab zapisu to ta sama funkcja, tyle że wystawiona przez HTTP zamiast MCP. Przydatne do ręcznego dodawania rzeczy z przeglądarki, gdy nie jestem w sesji Claude Code.

Co straciłem

Przeglądarkę plików. Obsidian ma widok drzewa, filtry tagów, widok grafu. Korzystałem z tego sporadycznie. Jeśli muszę przejrzeć, co jest wczytane, web UI ma pole wyszukiwania. Wystarczy.

Dostęp offline. Jeśli stack Docker jest wyłączony, dokumenty są niedostępne. Jest skrypt query.py podłączający się bezpośrednio do pgvectora, ale do swobodnego przeglądania potrzeba działającego serwisu.

Historia zmian per plik. Stary vault był w repozytorium git. Zmiany były śledzone. Teraz mam wiersze w pgvectorze. Upsert nadpisuje stare wersje. Mógłbym dodać tabelę wersjonowania, ale jeszcze jej nie potrzebuję.

Co zyskałem

Koniec rozjeżdżania się danych. Jest jedno źródło prawdy i to baza danych. Gdy zapiszę dokument przez rag_save, to, co Claude przeszukuje, i to, co napisałem, jest identyczne. Żadnego kroku synchronizacji, żadnego zastanawiania się, która wersja jest aktualna.

Koniec z ceremonią stagingu. Stary przepływ (pisz, kopiuj na NAS, czekaj na watchera, potwierdź) zniknął dla zwykłych dokumentów.

Zniknęła też jedna kategoria myślowa. Kiedyś myślałem w trybie “czy to jest w Obsidianie czy w RAG?”. Teraz jest tylko RAG.

System typów dokumentów

Każdy dokument ma jawny typ: text, runbook, ticket, email, guide, report, decision. Decyzje mają własny typ specjalnie po to, żeby szukając “dlaczego zrobiliśmy X” dostać dokument decyzyjny, a nie runbook, który X tylko wspomina.

W praktyce przy moim rozmiarze korpusu (~320 dokumentów) retrieval nie jest tak precyzyjny, ale etykiety są przydatne przy czytaniu wyników.

Wymiary wektorów, krótko

Przy okazji tej migracji analizowałem, czy zwiększyć wymiary embeddingów z 1536 do 3072. Gemini Embedding 2 obsługuje do 3072. Różnica jakości jest mierzalna na benchmarkach, ale przy 320 dokumentach i specyficznych zapytaniach do wewnętrznych dokumentów nie jest zauważalna. Sequential scan w tej skali jest szybki niezależnie od wymiaru. Zostałem przy 1536.

Jeśli korpus znacząco urośnie albo jakość retrieval zacznie spadać, to zmiana jednej linii w konfiguracji i ponowne wczytanie embeddingów. Przy obecnej liczbie dokumentów trwa to poniżej minuty.

Wynik praktyczny

RAG jest teraz jedynym systemem dokumentacji. Cztery kontenery Docker, jedna baza pgvector, dwa narzędzia MCP. Hooki i plik reguł zarządzają tym, kiedy asystent przeszukuje automatycznie. Podsumowania sesji trafiają do pamięci, watcher je wczytuje i pojawiają się w kolejnych sesjach. Pętla jest zamknięta.

Prosty system. Złożoność jest w treści, nie w infrastrukturze.