LLM Wiki pod maską: Python, Obsidian i Gemini
Spis treści
W pierwszym wpisie na LinkedIn opisałem koncepcję własnego LLM Wiki jako alternatywę dla standardowego podejścia RAG. Zamiast wektoryzować i przeszukiwać dokumenty przy każdym zapytaniu, system jednorazowo kompiluje i strukturyzuje wiedzę.
Poniżej omawiam architekturę i kod z repozytorium llm_wiki na GitHubie. Architektura celowo odrzuca bazy wektorowe na rzecz pełnego okna kontekstowego modeli Google Gemini i ścisłej strukturyzacji danych wejściowych.
Struktura danych i wymuszanie schematu (pydantic_models.py) #
Domyślnym wyjściem modelu LLM jest nieustrukturyzowany tekst, co utrudnia automatyzację. Aby zintegrować model z systemem plików, stosuję bibliotekę Pydantic do zdefiniowania sztywnego schematu wyjściowego (JSON). Model działa tu jako agent decyzyjny operujący na notatkach:
1# --- Python ---
2from pydantic import BaseModel, Field
3from typing import List
4
5class WikiAction(BaseModel):
6 action_type: str = Field(description="Typ akcji: 'CREATE', 'UPDATE' lub 'LOG'")
7 file_path: str = Field(description="Ścieżka do pliku w folderze wiki/, np. 'wiki/sztuczna_inteligencja.md'")
8 title: str = Field(description="Tytuł notatki (używany jako nazwa pliku i frontmatter)")
9 content: str = Field(description="Pełna treść notatki w formacie Markdown z linkami [[dwunawiasowymi]]. Jeśli UPDATE, podaj scaloną, zaktualizowaną treść.")
10 log_message: str = Field(description="Krótki opis zmiany do zapisania w logu")
11
12class WikiCompilationResult(BaseModel):
13 actions: List[WikiAction] = Field(description="Lista operacji na plikach wiki niezbędnych do przetworzenia źródła")
Asymilacja i strukturyzacja wiedzy (kompilacja_bazy.py) #
Głównym interfejsem bazy jest Obsidian. Nowa, surowa notatka wrzucona do folderu raw wyzwala proces kompilacji. Zanim model przetworzy nowy plik, funkcja get_current_wiki_context() ładuje zmapowaną strukturę istniejącego repozytorium.
Dzięki temu i narzuconemu schematowi WikiCompilationResult, model podejmuje decyzję kontekstową: tworzy nowy plik lub aktualizuje treść w już istniejącym.
1# --- Python ---
2import os
3import glob
4from google import genai
5from google.genai import types
6from pydantic_models import WikiCompilationResult
7from dotenv import load_dotenv
8
9load_dotenv() # Klucze API ładowane bezpiecznie z pliku .env (wykluczonego w .gitignore)
10client = genai.Client()
11
12def get_current_wiki_context() -> str:
13 """Zbiera strukturę i spis treści obecnego wiki."""
14 context = "OBECNY STAN WIKI:\n"
15 wiki_files = glob.glob("/Users/mpendolski/Obsidian/llm-wiki/wiki/*.md")
16
17 if not wiki_files:
18 return context + "Wiki jest puste. Brak istniejących notatek."
19
20 for file_path in wiki_files:
21 with open(file_path, "r", encoding="utf-8") as f:
22 lines = [f.readline() for _ in range(15)]
23 context += f"--- PLIK: {file_path} ---\n" + "".join(lines) + "\n"
24 return context
25
26def compile_source_to_wiki(raw_file_path: str):
27 print(f"⚡ Przetwarzanie pliku: {raw_file_path}...")
28
29 with open(raw_file_path, "r", encoding="utf-8") as f:
30 raw_content = f.read()
31
32 wiki_context = get_current_wiki_context()
33 system_instruction = open("/Users/mpendolski/Obsidian/llm-wiki/AGENTS.md", "r", encoding="utf-8").read()
34
35 user_prompt = f"""
36 Zintegruj poniższy materiał źródłowy z moim Wiki.
37
38 {wiki_context}
39
40 MATERIAŁ ŹRÓDŁOWY DO PRZETWORZENIA:
41 {raw_content}
42 """
43
44 response = client.models.generate_content(
45 model='gemini-2.5-flash',
46 contents=user_prompt,
47 config=types.GenerateContentConfig(
48 system_instruction=system_instruction,
49 temperature=0.1,
50 response_mime_type="application/json",
51 response_schema=WikiCompilationResult,
52 ),
53 )
54
55 result: WikiCompilationResult = response.parsed
56 # [Dalsza część skryptu zapisuje wygenerowane pliki Markdown i aktualizuje log]
Odpytywanie bazy bez halucynacji (odpytanie_bazy.py) #
Zamiast wyszukiwania semantycznego po fragmentach (chunkach), skrypt ładuje pełną zawartość skompilowanych notatek z Obsidiana wprost do okna kontekstowego Gemini. System prompt ściśle ogranicza model do dostarczonych danych i wymusza tagowanie pojęć (np. [[Nazwa Notatki]]). Niska temperatura (0.2) minimalizuje ryzyko halucynacji i zapewnia deterministyczne wyjście.
1# --- Python ---
2import os
3import glob
4from google import genai
5from google.genai import types
6from dotenv import load_dotenv
7
8load_dotenv()
9client = genai.Client()
10
11def load_compiled_wiki() -> str:
12 """Ładuje PEŁNĄ zawartość wszystkich skompilowanych notatek wiki."""
13 full_wiki_content = ""
14 wiki_files = glob.glob("/Users/mpendolski/Obsidian/llm-wiki/wiki/*.md")
15
16 for file_path in wiki_files:
17 if "log.md" in file_path:
18 continue
19
20 with open(file_path, "r", encoding="utf-8") as f:
21 filename = os.path.basename(file_path).replace(".md", "")
22 full_wiki_content += f"\n=== NOTATKA: {filename} ===\n"
23 full_wiki_content += f.read() + "\n"
24
25 return full_wiki_content
26
27def ask_wiki(question: str):
28 wiki_data = load_compiled_wiki()
29
30 system_instruction = """
31 Jesteś ekspertem analizującym bazę wiedzy użytkownika.
32 Twoje odpowiedzi muszą opierać się WYŁĄCZNIE na dostarczonych notatkach z sekcji KOMPILACJA WIKI.
33 Jeśli w bazie nie ma informacji potrzebnych do odpowiedzi, zwróć komunikat: 'Brak danych w wiki'.
34 Kiedy odwołujesz się do pojęcia, które istnieje w bazie, użyj formatu [[Nazwa Notatki]], tak jak w Obsidianie.
35 """
36
37 prompt = f"""
38 KOMPILACJA WIKI:
39 {wiki_data}
40
41 PYTANIE UŻYTKOWNIKA:
42 {question}
43 """
44
45 response = client.models.generate_content(
46 model='gemini-3.5-flash',
47 contents=prompt,
48 config=types.GenerateContentConfig(
49 system_instruction=system_instruction,
50 temperature=0.2,
51 ),
52 )
53 print(response.text)
Podsumowanie #
LLM Wiki automatyzuje asymilację luźnych notatek i zamienia je w ustrukturyzowaną bazę Markdown, pozwalając na analizę danych i wyciąganie wniosków w oparciu o pełny kontekst projektowy. Cały udostępniony kod znajdziesz w repozytorium: GitHub - llm_wiki.